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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
|
<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<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 getNoiseStream({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());
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(async t => {
const pc = createPeerConnectionWithCleanup(t);
const pc2 = createPeerConnectionWithCleanup(t);
const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio');
pc.addTrack(sendtrack, mediaStream);
exchangeIceCandidates(pc, pc2);
await Promise.all([
exchangeOfferAnswer(pc, pc2),
new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
]);
const statsReport = await pc.getStats();
getRequiredStats(statsReport, 'peer-connection');
getRequiredStats(statsReport, 'outbound-rtp');
}, 'getStats() track with stream returns peer-connection and outbound-rtp stats');
promise_test(async t => {
const pc = createPeerConnectionWithCleanup(t);
const pc2 = createPeerConnectionWithCleanup(t);
const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio');
pc.addTrack(sendtrack);
exchangeIceCandidates(pc, pc2);
await Promise.all([
exchangeOfferAnswer(pc, pc2),
new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
]);
const statsReport = await pc.getStats();
getRequiredStats(statsReport, 'peer-connection');
getRequiredStats(statsReport, 'outbound-rtp');
}, 'getStats() track without stream returns peer-connection and outbound-rtp stats');
promise_test(async t => {
const pc = createPeerConnectionWithCleanup(t);
const pc2 = createPeerConnectionWithCleanup(t);
const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio');
pc.addTrack(sendtrack, mediaStream);
exchangeIceCandidates(pc, pc2);
await Promise.all([
exchangeOfferAnswer(pc, pc2),
new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
]);
const statsReport = await pc.getStats();
assert_stats_report_has_stats(statsReport, ['outbound-rtp']);
}, 'getStats() audio outbound-rtp contains all mandatory stats');
promise_test(async t => {
const pc = createPeerConnectionWithCleanup(t);
const pc2 = createPeerConnectionWithCleanup(t);
const [sendtrack, mediaStream] = await getTrackFromUserMedia('video');
pc.addTrack(sendtrack, mediaStream);
exchangeIceCandidates(pc, pc2);
await Promise.all([
exchangeOfferAnswer(pc, pc2),
new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
]);
const statsReport = await pc.getStats();
assert_stats_report_has_stats(statsReport, ['outbound-rtp']);
}, 'getStats() video outbound-rtp contains all mandatory stats');
promise_test(async t => {
const pc = createPeerConnectionWithCleanup(t);
const pc2 = createPeerConnectionWithCleanup(t);
const [audioTrack, audioStream] = await getTrackFromUserMedia('audio');
pc.addTrack(audioTrack, audioStream);
const [videoTrack, videoStream] = await getTrackFromUserMedia('video');
pc.addTrack(videoTrack, videoStream);
exchangeIceCandidates(pc, pc2);
await Promise.all([
exchangeOfferAnswer(pc, pc2),
new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
]);
const statsReport = await pc.getStats();
validateStatsReport(statsReport);
}, 'getStats() audio and video validate all mandatory stats');
/*
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 [sendtrack, mediaStream] = await getTrackFromUserMedia('audio');
pc.addTrack(sendtrack, mediaStream);
exchangeIceCandidates(pc, pc2);
await Promise.all([
exchangeOfferAnswer(pc, pc2),
new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
]);
const stats = await pc.getStats(sendtrack);
getRequiredStats(stats, 'outbound-rtp');
}, `getStats() on track associated with RTCRtpSender 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);
getRequiredStats(stats, 'inbound-rtp');
}, `getStats() on track associated with RTCRtpReceiver should return stats report containing inbound-rtp stats`);
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);
getRequiredStats(stats, 'inbound-rtp');
}, `getStats() inbound-rtp contains all mandatory 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",
"media-source",
"peer-connection",
"data-channel",
"sender",
"receiver",
"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');
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');
}
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`);
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const [track, mediaStream] = await getTrackFromUserMedia('audio');
pc.addTransceiver(track);
pc.addTransceiver(track);
await promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track));
}, `getStats(track) should not work if multiple senders have the same track`);
promise_test(async t => {
const kMinimumTimeElapsedBetweenGetStatsCallsMs = 500;
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const t0 = Math.floor(performance.now());
const t0Stats = getRequiredStats(await pc.getStats(), 'peer-connection');
await new Promise(
r => t.step_timeout(r, kMinimumTimeElapsedBetweenGetStatsCallsMs));
const t1Stats = getRequiredStats(await pc.getStats(), 'peer-connection');
const t1 = Math.ceil(performance.now());
const maximumTimeElapsedBetweenGetStatsCallsMs = t1 - t0;
const deltaTimestampMs = t1Stats.timestamp - t0Stats.timestamp;
// The delta must be at least the time we waited between calls.
assert_greater_than_equal(deltaTimestampMs,
kMinimumTimeElapsedBetweenGetStatsCallsMs);
// The delta must be at most the time elapsed before the first getStats()
// call and after the second getStats() call.
assert_less_than_equal(deltaTimestampMs,
maximumTimeElapsedBetweenGetStatsCallsMs);
}, `RTCStats.timestamp increases with time passing`);
</script>
|