MediaStreamTrack-video-stats.https.html (14666B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <button id="button">User gesture</button> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="/resources/testdriver.js"></script> 8 <script src="/resources/testdriver-vendor.js"></script> 9 <script> 10 'use strict'; 11 12 async function getFrameStatsUntil(track, condition) { 13 while (true) { 14 const stats = track.stats.toJSON(); 15 if (condition(stats)) { 16 return stats; 17 } 18 // Repeat in the next task execution cycle. 19 await Promise.resolve(); 20 } 21 } 22 23 promise_test(async t => { 24 const stream = await navigator.mediaDevices.getUserMedia({video:true}); 25 const [track] = stream.getTracks(); 26 t.add_cleanup(() => track.stop()); 27 28 const firstStats = 29 await getFrameStatsUntil(track, stats => stats.totalFrames > 0); 30 await getFrameStatsUntil(track, 31 stats => stats.totalFrames > firstStats.totalFrames); 32 }, `totalFrames increases over time`); 33 34 promise_test(async t => { 35 const stream = await navigator.mediaDevices.getUserMedia({video:true}); 36 const [track] = stream.getTracks(); 37 t.add_cleanup(() => track.stop()); 38 39 // `deliveredFrames` increments for each deliverable frame, even if the 40 // `track` does not have any sink. 41 const firstStats = await getFrameStatsUntil( 42 track, stats => stats.deliveredFrames > 0); 43 await getFrameStatsUntil( 44 track, stats => stats.deliveredFrames > firstStats.deliveredFrames); 45 }, `deliveredFrames increases, even without sinks`); 46 47 promise_test(async t => { 48 const stream = await navigator.mediaDevices.getUserMedia({ 49 video:{frameRate:{ideal:20}} 50 }); 51 const [track] = stream.getTracks(); 52 t.add_cleanup(() => track.stop()); 53 54 // Assert test prerequisite is met: frames will be discarded if the track is 55 // opened with a higher frame rate than we apply after it is opened. 56 assert_greater_than(track.getSettings().frameRate, 10); 57 await track.applyConstraints({frameRate:{ideal:10}}); 58 59 await getFrameStatsUntil(track, stats => stats.discardedFrames > 0); 60 }, `discardedFrames increases when frameRate decimation is happening`); 61 62 promise_test(async t => { 63 const stream = await navigator.mediaDevices.getUserMedia({video:true}); 64 const [track] = stream.getTracks(); 65 t.add_cleanup(() => track.stop()); 66 67 // Hold a reference directly to the [SameObject] stats, bypassing the 68 // `track.stats` getter in the subsequent getting of `totalFrames`. 69 const stats = track.stats; 70 const firstTotalFrames = stats.totalFrames; 71 while (stats.totalFrames == firstTotalFrames) { 72 await Promise.resolve(); 73 } 74 assert_greater_than(stats.totalFrames, firstTotalFrames); 75 }, `Counters increase even if we don't call the track.stats getter`); 76 77 promise_test(async t => { 78 const stream = await navigator.mediaDevices.getUserMedia({video:true}); 79 const [track] = stream.getTracks(); 80 t.add_cleanup(() => track.stop()); 81 82 const firstTotalFrames = track.stats.totalFrames; 83 // Busy-loop for 100 ms, all within the same task execution cycle. 84 const firstTimeMs = performance.now(); 85 while (performance.now() - firstTimeMs < 100) {} 86 // The frame counter should not have changed. 87 assert_equals(track.stats.totalFrames, firstTotalFrames); 88 }, `Counters do not increase in the same task execution cycle`); 89 90 promise_test(async t => { 91 const stream = await navigator.mediaDevices.getUserMedia({ 92 video:{frameRate:{ideal:20}} 93 }); 94 const [track] = stream.getTracks(); 95 t.add_cleanup(() => track.stop()); 96 97 // Assert test prerequisite is met: frames will be discarded if the track is 98 // opened with a higher frame rate than we apply after it is opened. 99 assert_greater_than(track.getSettings().frameRate, 10); 100 await track.applyConstraints({frameRate:{ideal:10}}); 101 102 // Wait until we have both delivered and discarded frames. 103 const stats = await getFrameStatsUntil(track, stats => 104 stats.deliveredFrames > 0 && stats.discardedFrames > 0); 105 106 // This test assumes that no frames are dropped, otherwise `totalFrames` can 107 // be greater than the sum of `deliveredFrames` and `discardedFrames`. 108 assert_equals(stats.totalFrames, 109 stats.deliveredFrames + stats.discardedFrames); 110 }, `totalFrames is the sum of deliveredFrames and discardedFrames`); 111 112 promise_test(async t => { 113 const stream = await navigator.mediaDevices.getUserMedia({video:true}); 114 const [track] = stream.getTracks(); 115 t.add_cleanup(() => track.stop()); 116 117 const a = track.stats; 118 await getFrameStatsUntil(track, stats => stats.totalFrames > 0); 119 const b = track.stats; 120 // The counters may have changed, but `a` and `b` are still the same object. 121 assert_equals(a, b); 122 }, `SameObject policy applies`); 123 124 promise_test(async t => { 125 const stream = await navigator.mediaDevices.getUserMedia({ 126 video:{frameRate:{ideal:20}} 127 }); 128 const [track] = stream.getTracks(); 129 t.add_cleanup(() => track.stop()); 130 131 // Assert test prerequisite is met: frames will be discarded if the track is 132 // opened with a higher frame rate than we apply after it is opened. 133 assert_greater_than(track.getSettings().frameRate, 10); 134 await track.applyConstraints({frameRate:{ideal:10}}); 135 136 // Wait for media to flow before disabling the `track`. 137 const initialStats = await getFrameStatsUntil(track, stats => 138 stats.deliveredFrames > 0 && stats.discardedFrames > 0 && 139 stats.totalFrames > 10); 140 track.enabled = false; 141 // Upon disabling, the counters are not reset. 142 const disabledSnapshot = track.stats.toJSON(); 143 assert_greater_than_equal(disabledSnapshot.deliveredFrames, 144 initialStats.deliveredFrames); 145 assert_greater_than_equal(disabledSnapshot.discardedFrames, 146 initialStats.discardedFrames); 147 assert_greater_than_equal(disabledSnapshot.totalFrames, 148 initialStats.totalFrames); 149 150 // Wait enough time that frames should have been produced. 151 await new Promise(r => t.step_timeout(r, 500)); 152 153 // Frame metrics should be frozen, but because `enabled = false` does not 154 // return a promise, we allow some lee-way in case a frame was still in flight 155 // during the disabling. 156 assert_approx_equals( 157 track.stats.deliveredFrames, disabledSnapshot.deliveredFrames, 1); 158 assert_approx_equals( 159 track.stats.discardedFrames, disabledSnapshot.discardedFrames, 1); 160 assert_approx_equals( 161 track.stats.totalFrames, disabledSnapshot.totalFrames, 1); 162 }, `Stats are frozen while disabled`); 163 164 promise_test(async t => { 165 const stream = await navigator.mediaDevices.getUserMedia({ 166 video:{frameRate:{ideal:20}} 167 }); 168 const [track] = stream.getTracks(); 169 t.add_cleanup(() => track.stop()); 170 171 // Assert test prerequisite is met: frames will be discarded if the track is 172 // opened with a higher frame rate than we apply after it is opened. 173 assert_greater_than(track.getSettings().frameRate, 10); 174 await track.applyConstraints({frameRate:{ideal:10}}); 175 176 // Wait for media to flow before disabling the `track`. 177 const initialStats = await getFrameStatsUntil(track, stats => 178 stats.deliveredFrames > 10 && stats.discardedFrames > 10); 179 track.enabled = false; 180 181 // Re-enable the track. The stats counters should be greater than or equal to 182 // what they were previously. 183 track.enabled = true; 184 assert_greater_than_equal(track.stats.deliveredFrames, 185 initialStats.deliveredFrames); 186 assert_greater_than_equal(track.stats.discardedFrames, 187 initialStats.discardedFrames); 188 assert_greater_than_equal(track.stats.totalFrames, 189 initialStats.totalFrames); 190 }, `Disabling and re-enabling does not reset the counters`); 191 192 promise_test(async t => { 193 const stream = await navigator.mediaDevices.getUserMedia({ 194 video:{frameRate:{ideal:20}} 195 }); 196 const [originalTrack] = stream.getTracks(); 197 t.add_cleanup(() => originalTrack.stop()); 198 199 // Assert test prerequisite is met: frames will be discarded if the track is 200 // opened with a higher frame rate than we apply after it is opened. 201 assert_greater_than(originalTrack.getSettings().frameRate, 10); 202 await originalTrack.applyConstraints({frameRate:{ideal:10}}); 203 204 // Wait for media to flow before disabling the `track`. 205 await getFrameStatsUntil(originalTrack, stats => 206 stats.deliveredFrames > 0 && stats.discardedFrames > 0); 207 originalTrack.enabled = false; 208 const originalTrackInitialStats = originalTrack.stats.toJSON(); 209 210 // Clone the track, its counters should be zero initially. 211 // This is not racy because the cloned track is also disabled. 212 const clonedTrack = originalTrack.clone(); 213 t.add_cleanup(() => clonedTrack.stop()); 214 const clonedTrackStats = clonedTrack.stats.toJSON(); 215 assert_equals(clonedTrackStats.deliveredFrames, 0); 216 assert_equals(clonedTrackStats.discardedFrames, 0); 217 assert_equals(clonedTrackStats.totalFrames, 0); 218 219 // Enabled the cloned track and wait for media to flow. 220 clonedTrack.enabled = true; 221 await getFrameStatsUntil(clonedTrack, stats => 222 stats.deliveredFrames > 0 && stats.discardedFrames > 0); 223 224 // This does not affect the original track's stats, which are still frozen due 225 // to the original track being disabled. 226 assert_equals(originalTrack.stats.deliveredFrames, 227 originalTrackInitialStats.deliveredFrames); 228 assert_equals(originalTrack.stats.discardedFrames, 229 originalTrackInitialStats.discardedFrames); 230 assert_equals(originalTrack.stats.totalFrames, 231 originalTrackInitialStats.totalFrames); 232 }, `New stats baselines when a track is cloned from a disabled track`); 233 234 promise_test(async t => { 235 const stream = await navigator.mediaDevices.getUserMedia({ 236 video:{frameRate:{ideal:20}} 237 }); 238 const [originalTrack] = stream.getTracks(); 239 t.add_cleanup(() => originalTrack.stop()); 240 241 // Assert test prerequisite is met: frames will be discarded if the track is 242 // opened with a higher frame rate than we apply after it is opened. 243 assert_greater_than(originalTrack.getSettings().frameRate, 10); 244 await originalTrack.applyConstraints({frameRate:{ideal:10}}); 245 246 // Wait for media to flow. 247 await getFrameStatsUntil(originalTrack, stats => 248 stats.deliveredFrames > 0 && stats.discardedFrames > 0); 249 250 // Clone the track. While its counters should initially be zero, it would be 251 // racy to assert that they are exactly zero because media is flowing. 252 const clonedTrack = originalTrack.clone(); 253 t.add_cleanup(() => clonedTrack.stop()); 254 255 // Ensure that as media continues to flow, the cloned track will necessarily 256 // have less frames than the original track on all accounts since its counters 257 // will have started from zero. 258 const clonedTrackStats = await getFrameStatsUntil(clonedTrack, stats => 259 stats.deliveredFrames > 0 && stats.discardedFrames > 0); 260 assert_less_than(clonedTrackStats.deliveredFrames, 261 originalTrack.stats.deliveredFrames); 262 assert_less_than(clonedTrackStats.discardedFrames, 263 originalTrack.stats.discardedFrames); 264 assert_less_than(clonedTrackStats.totalFrames, 265 originalTrack.stats.totalFrames); 266 }, `New stats baselines when a track is cloned from an enabled track`); 267 268 promise_test(async t => { 269 const stream = await navigator.mediaDevices.getUserMedia({ 270 video:{frameRate:{ideal:20}} 271 }); 272 const [originalTrack] = stream.getTracks(); 273 t.add_cleanup(() => originalTrack.stop()); 274 275 // Assert test prerequisite is met: frames will be discarded if the track is 276 // opened with a higher frame rate than we apply after it is opened. 277 assert_greater_than(originalTrack.getSettings().frameRate, 10); 278 await originalTrack.applyConstraints({frameRate:{ideal:10}}); 279 280 // Wait for media to flow. 281 await getFrameStatsUntil(originalTrack, stats => 282 stats.deliveredFrames > 0 && stats.discardedFrames > 0); 283 284 // Clone and wait for media to flow. 285 const cloneA = originalTrack.clone(); 286 t.add_cleanup(() => cloneA.stop()); 287 await getFrameStatsUntil(cloneA, stats => 288 stats.deliveredFrames > 0 && stats.discardedFrames > 0); 289 290 // Clone the clone and wait for media to flow. 291 const cloneB = cloneA.clone(); 292 t.add_cleanup(() => cloneB.stop()); 293 await getFrameStatsUntil(cloneB, stats => 294 stats.deliveredFrames > 0 && stats.discardedFrames > 0); 295 296 // Because every clone reset its counters and every waits for media before 297 // cloning, this must be true: originalStats > cloneAStats > cloneBStats. 298 const originalStats = originalTrack.stats.toJSON(); 299 const cloneAStats = cloneA.stats.toJSON(); 300 const cloneBStats = cloneB.stats.toJSON(); 301 assert_greater_than(originalStats.totalFrames, cloneAStats.totalFrames); 302 assert_greater_than(cloneAStats.totalFrames, cloneBStats.totalFrames); 303 }, `New stats baselines for the clone of a clone`); 304 305 promise_test(async t => { 306 const stream = await navigator.mediaDevices.getUserMedia({video:true}); 307 const [originalTrack] = stream.getTracks(); 308 t.add_cleanup(() => originalTrack.stop()); 309 310 // Wait for some frames and assert that no frames are discarded. 311 const firstStats = await getFrameStatsUntil(originalTrack, stats => 312 stats.deliveredFrames > 20); 313 assert_equals(firstStats.discardedFrames, 0); 314 // Make a clone that discards almost all frames. This should not affect the 315 // discarded frames counter of the original track. 316 const clonedTrack = originalTrack.clone(); 317 await clonedTrack.applyConstraints({frameRate:{ideal:1}}); 318 // Wait for some more frames. There should still be no frames discarded. 319 const secondStats = await getFrameStatsUntil(originalTrack, stats => 320 stats.deliveredFrames > 40); 321 assert_equals(secondStats.discardedFrames, 0); 322 }, `A low FPS clone does not affect the original track's discardedFrames`); 323 324 promise_test(async t => { 325 const canvas = document.createElement('canvas'); 326 const stream = canvas.captureStream(10); 327 const [track] = stream.getTracks(); 328 t.add_cleanup(() => track.stop()); 329 330 assert_equals(track.stats, null); 331 }, `track.stats is null on non-device tracks, such as canvas`); 332 333 promise_test(async t => { 334 // getDisplayMedia() requires inducing a user gesture. 335 const p = new Promise(r => button.onclick = r); 336 await test_driver.click(button); 337 await p; 338 339 const stream = await navigator.mediaDevices.getDisplayMedia({video:true}); 340 const [track] = stream.getTracks(); 341 t.add_cleanup(() => track.stop()); 342 343 await getFrameStatsUntil(track, stats => stats.totalFrames > 0) 344 }, `track.stats is supported on getDisplayMedia tracks`); 345 </script>