summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/mediacapture-extensions/MediaStreamTrack-video-stats.https.html
blob: 374a22c7caf347e628f9053b032d65a6ffc56fab (plain)
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>
<meta name="timeout" content="long">
<button id="button">User gesture</button>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script>
'use strict';

async function getFrameStatsUntil(track, condition) {
  while (true) {
    const stats = track.stats.toJSON();
    if (condition(stats)) {
      return stats;
    }
    // Repeat in the next task execution cycle.
    await Promise.resolve();
  }
}

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({video:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  const firstStats =
      await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
  await getFrameStatsUntil(track,
                           stats => stats.totalFrames > firstStats.totalFrames);
}, `totalFrames increases over time`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({video:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // `deliveredFrames` increments for each deliverable frame, even if the
  // `track` does not have any sink.
  const firstStats = await getFrameStatsUntil(
      track, stats => stats.deliveredFrames > 0);
  await getFrameStatsUntil(
      track, stats => stats.deliveredFrames > firstStats.deliveredFrames);
}, `deliveredFrames increases, even without sinks`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({
          video:{frameRate:{ideal:20}}
      });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Assert test prerequisite is met: frames will be discarded if the track is
  // opened with a higher frame rate than we apply after it is opened.
  assert_greater_than(track.getSettings().frameRate, 10);
  await track.applyConstraints({frameRate:{ideal:10}});

  await getFrameStatsUntil(track, stats => stats.discardedFrames > 0);
}, `discardedFrames increases when frameRate decimation is happening`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({video:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Hold a reference directly to the [SameObject] stats, bypassing the
  // `track.stats` getter in the subsequent getting of `totalFrames`.
  const stats = track.stats;
  const firstTotalFrames = stats.totalFrames;
  while (stats.totalFrames == firstTotalFrames) {
    await Promise.resolve();
  }
  assert_greater_than(stats.totalFrames, firstTotalFrames);
}, `Counters increase even if we don't call the track.stats getter`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({video:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  const firstTotalFrames = track.stats.totalFrames;
  // Busy-loop for 100 ms, all within the same task execution cycle.
  const firstTimeMs = performance.now();
  while (performance.now() - firstTimeMs < 100) {}
  // The frame counter should not have changed.
  assert_equals(track.stats.totalFrames, firstTotalFrames);
}, `Counters do not increase in the same task execution cycle`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({
          video:{frameRate:{ideal:20}}
      });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Assert test prerequisite is met: frames will be discarded if the track is
  // opened with a higher frame rate than we apply after it is opened.
  assert_greater_than(track.getSettings().frameRate, 10);
  await track.applyConstraints({frameRate:{ideal:10}});

  // Wait until we have both delivered and discarded frames.
  const stats = await getFrameStatsUntil(track, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0);

  // This test assumes that no frames are dropped, otherwise `totalFrames` can
  // be greater than the sum of `deliveredFrames` and `discardedFrames`.
  assert_equals(stats.totalFrames,
                stats.deliveredFrames + stats.discardedFrames);
}, `totalFrames is the sum of deliveredFrames and discardedFrames`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({video:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  const a = track.stats;
  await getFrameStatsUntil(track, stats => stats.totalFrames > 0);
  const b = track.stats;
  // The counters may have changed, but `a` and `b` are still the same object.
  assert_equals(a, b);
}, `SameObject policy applies`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({
          video:{frameRate:{ideal:20}}
      });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Assert test prerequisite is met: frames will be discarded if the track is
  // opened with a higher frame rate than we apply after it is opened.
  assert_greater_than(track.getSettings().frameRate, 10);
  await track.applyConstraints({frameRate:{ideal:10}});

  // Wait for media to flow before disabling the `track`.
  const initialStats = await getFrameStatsUntil(track, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0 &&
      stats.totalFrames > 10);
  track.enabled = false;
  // Upon disabling, the counters are not reset.
  const disabledSnapshot = track.stats.toJSON();
  assert_greater_than_equal(disabledSnapshot.deliveredFrames,
                            initialStats.deliveredFrames);
  assert_greater_than_equal(disabledSnapshot.discardedFrames,
                            initialStats.discardedFrames);
  assert_greater_than_equal(disabledSnapshot.totalFrames,
                            initialStats.totalFrames);

  // Wait enough time that frames should have been produced.
  await new Promise(r => t.step_timeout(r, 500));

  // Frame metrics should be frozen, but because `enabled = false` does not
  // return a promise, we allow some lee-way in case a frame was still in flight
  // during the disabling.
  assert_approx_equals(
      track.stats.deliveredFrames, disabledSnapshot.deliveredFrames, 1);
  assert_approx_equals(
      track.stats.discardedFrames, disabledSnapshot.discardedFrames, 1);
  assert_approx_equals(
      track.stats.totalFrames, disabledSnapshot.totalFrames, 1);
}, `Stats are frozen while disabled`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({
          video:{frameRate:{ideal:20}}
      });
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  // Assert test prerequisite is met: frames will be discarded if the track is
  // opened with a higher frame rate than we apply after it is opened.
  assert_greater_than(track.getSettings().frameRate, 10);
  await track.applyConstraints({frameRate:{ideal:10}});

  // Wait for media to flow before disabling the `track`.
  const initialStats = await getFrameStatsUntil(track, stats =>
      stats.deliveredFrames > 10 && stats.discardedFrames > 10);
  track.enabled = false;

  // Re-enable the track. The stats counters should be greater than or equal to
  // what they were previously.
  track.enabled = true;
  assert_greater_than_equal(track.stats.deliveredFrames,
                            initialStats.deliveredFrames);
  assert_greater_than_equal(track.stats.discardedFrames,
                            initialStats.discardedFrames);
  assert_greater_than_equal(track.stats.totalFrames,
                            initialStats.totalFrames);
}, `Disabling and re-enabling does not reset the counters`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({
          video:{frameRate:{ideal:20}}
      });
  const [originalTrack] = stream.getTracks();
  t.add_cleanup(() => originalTrack.stop());

  // Assert test prerequisite is met: frames will be discarded if the track is
  // opened with a higher frame rate than we apply after it is opened.
  assert_greater_than(originalTrack.getSettings().frameRate, 10);
  await originalTrack.applyConstraints({frameRate:{ideal:10}});

  // Wait for media to flow before disabling the `track`.
  await getFrameStatsUntil(originalTrack, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0);
  originalTrack.enabled = false;
  const originalTrackInitialStats = originalTrack.stats.toJSON();

  // Clone the track, its counters should be zero initially.
  // This is not racy because the cloned track is also disabled.
  const clonedTrack = originalTrack.clone();
  t.add_cleanup(() => clonedTrack.stop());
  const clonedTrackStats = clonedTrack.stats.toJSON();
  assert_equals(clonedTrackStats.deliveredFrames, 0);
  assert_equals(clonedTrackStats.discardedFrames, 0);
  assert_equals(clonedTrackStats.totalFrames, 0);

  // Enabled the cloned track and wait for media to flow.
  clonedTrack.enabled = true;
  await getFrameStatsUntil(clonedTrack, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0);

  // This does not affect the original track's stats, which are still frozen due
  // to the original track being disabled.
  assert_equals(originalTrack.stats.deliveredFrames,
                originalTrackInitialStats.deliveredFrames);
  assert_equals(originalTrack.stats.discardedFrames,
                originalTrackInitialStats.discardedFrames);
  assert_equals(originalTrack.stats.totalFrames,
                originalTrackInitialStats.totalFrames);
}, `New stats baselines when a track is cloned from a disabled track`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({
          video:{frameRate:{ideal:20}}
      });
  const [originalTrack] = stream.getTracks();
  t.add_cleanup(() => originalTrack.stop());

  // Assert test prerequisite is met: frames will be discarded if the track is
  // opened with a higher frame rate than we apply after it is opened.
  assert_greater_than(originalTrack.getSettings().frameRate, 10);
  await originalTrack.applyConstraints({frameRate:{ideal:10}});

  // Wait for media to flow.
  await getFrameStatsUntil(originalTrack, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0);

  // Clone the track. While its counters should initially be zero, it would be
  // racy to assert that they are exactly zero because media is flowing.
  const clonedTrack = originalTrack.clone();
  t.add_cleanup(() => clonedTrack.stop());

  // Ensure that as media continues to flow, the cloned track will necessarily
  // have less frames than the original track on all accounts since its counters
  // will have started from zero.
  const clonedTrackStats = await getFrameStatsUntil(clonedTrack, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0);
  assert_less_than(clonedTrackStats.deliveredFrames,
                   originalTrack.stats.deliveredFrames);
  assert_less_than(clonedTrackStats.discardedFrames,
                   originalTrack.stats.discardedFrames);
  assert_less_than(clonedTrackStats.totalFrames,
                   originalTrack.stats.totalFrames);
}, `New stats baselines when a track is cloned from an enabled track`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({
          video:{frameRate:{ideal:20}}
      });
  const [originalTrack] = stream.getTracks();
  t.add_cleanup(() => originalTrack.stop());

  // Assert test prerequisite is met: frames will be discarded if the track is
  // opened with a higher frame rate than we apply after it is opened.
  assert_greater_than(originalTrack.getSettings().frameRate, 10);
  await originalTrack.applyConstraints({frameRate:{ideal:10}});

  // Wait for media to flow.
  await getFrameStatsUntil(originalTrack, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0);

  // Clone and wait for media to flow.
  const cloneA = originalTrack.clone();
  t.add_cleanup(() => cloneA.stop());
  await getFrameStatsUntil(cloneA, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0);

  // Clone the clone and wait for media to flow.
  const cloneB = cloneA.clone();
  t.add_cleanup(() => cloneB.stop());
  await getFrameStatsUntil(cloneB, stats =>
      stats.deliveredFrames > 0 && stats.discardedFrames > 0);

  // Because every clone reset its counters and every waits for media before
  // cloning, this must be true: originalStats > cloneAStats > cloneBStats.
  const originalStats = originalTrack.stats.toJSON();
  const cloneAStats = cloneA.stats.toJSON();
  const cloneBStats = cloneB.stats.toJSON();
  assert_greater_than(originalStats.totalFrames, cloneAStats.totalFrames);
  assert_greater_than(cloneAStats.totalFrames, cloneBStats.totalFrames);
}, `New stats baselines for the clone of a clone`);

promise_test(async t => {
  const stream = await navigator.mediaDevices.getUserMedia({video:true});
  const [originalTrack] = stream.getTracks();
  t.add_cleanup(() => originalTrack.stop());

  // Wait for some frames and assert that no frames are discarded.
  const firstStats = await getFrameStatsUntil(originalTrack, stats =>
      stats.deliveredFrames > 20);
  assert_equals(firstStats.discardedFrames, 0);
  // Make a clone that discards almost all frames. This should not affect the
  // discarded frames counter of the original track.
  const clonedTrack = originalTrack.clone();
  await clonedTrack.applyConstraints({frameRate:{ideal:1}});
  // Wait for some more frames. There should still be no frames discarded.
  const secondStats = await getFrameStatsUntil(originalTrack, stats =>
      stats.deliveredFrames > 40);
  assert_equals(secondStats.discardedFrames, 0);
}, `A low FPS clone does not affect the original track's discardedFrames`);

promise_test(async t => {
  const canvas = document.createElement('canvas');
  const stream = canvas.captureStream(10);
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  assert_equals(track.stats, null);
}, `track.stats is null on non-device tracks, such as canvas`);

promise_test(async t => {
  // getDisplayMedia() requires inducing a user gesture.
  const p = new Promise(r => button.onclick = r);
  await test_driver.click(button);
  await p;

  const stream = await navigator.mediaDevices.getDisplayMedia({video:true});
  const [track] = stream.getTracks();
  t.add_cleanup(() => track.stop());

  await getFrameStatsUntil(track, stats => stats.totalFrames > 0)
}, `track.stats is supported on getDisplayMedia tracks`);
</script>