test_encode_from_canvas.html (12366B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>WebCodecs performance test: Video Encoding</title> 5 </head> 6 <script src="/tests/SimpleTest/SimpleTest.js"></script> 7 <script> 8 const REALTIME = "realtime"; 9 const QUALITY = "quality"; 10 11 const RESULTS = {}; 12 RESULTS[REALTIME] = [ 13 { name: "frame-to-frame mean (key)", value: Infinity }, 14 { name: "frame-to-frame stddev (key)", value: Infinity }, 15 { name: "frame-dropping rate (key)", value: Infinity }, 16 { name: "frame-to-frame mean (non key)", value: Infinity }, 17 { name: "frame-to-frame stddev (non key)", value: Infinity }, 18 { name: "frame-dropping rate (non key)", value: Infinity }, 19 ]; 20 RESULTS[QUALITY] = [ 21 { name: "first encode to last output", value: Infinity }, 22 ]; 23 24 var perfMetadata = { 25 owner: "Media Team", 26 name: "WebCodecs Video Encoding", 27 description: "Test WebCodecs video encoding performance", 28 options: { 29 default: { 30 perfherder: true, 31 perfherder_metrics: [ 32 { 33 name: "realtime - frame-to-frame mean (key)", 34 unit: "ms", 35 shouldAlert: true, 36 }, 37 { 38 name: "realtime - frame-to-frame stddev (key)", 39 unit: "ms", 40 shouldAlert: true, 41 }, 42 { 43 name: "realtime - frame-dropping rate (key)", 44 unit: "ratio", 45 shouldAlert: true, 46 }, 47 { 48 name: "realtime - frame-to-frame mean (non key)", 49 unit: "ms", 50 shouldAlert: true, 51 }, 52 { 53 name: "realtime - frame-to-frame stddev (non key)", 54 unit: "ms", 55 shouldAlert: true, 56 }, 57 { 58 name: "realtime - frame-dropping rate (non key)", 59 unit: "ratio", 60 shouldAlert: true, 61 }, 62 { 63 name: "quality - first encode to last output", 64 unit: "ms", 65 shouldAlert: true, 66 }, 67 ], 68 verbose: true, 69 manifest: "perftest.toml", 70 manifest_flavor: "plain", 71 }, 72 }, 73 }; 74 75 function createCanvas(width, height) { 76 const canvas = document.createElement("canvas"); 77 canvas.width = width; 78 canvas.height = height; 79 document.body.appendChild(canvas); 80 return canvas; 81 } 82 83 function removeCanvas(canvas) { 84 const ctx = canvas.getContext("2d"); 85 ctx.clearRect(0, 0, canvas.width, canvas.height); 86 document.body.removeChild(canvas); 87 } 88 89 function drawClock(canvas) { 90 const ctx = canvas.getContext("2d"); 91 ctx.save(); 92 93 ctx.fillStyle = "#dfdacd"; 94 ctx.fillRect(0, 0, canvas.width, canvas.height); 95 96 let radius = canvas.height / 2; 97 ctx.translate(radius, radius); 98 radius = radius * 0.7; 99 100 drawFace(ctx, radius); 101 markHours(ctx, radius); 102 markMinutes(ctx, radius); 103 drawTime(ctx, radius); 104 105 ctx.restore(); 106 } 107 108 function drawFace(ctx, radius) { 109 ctx.save(); 110 ctx.beginPath(); 111 ctx.arc(0, 0, radius, 0, 2 * Math.PI); 112 ctx.fillStyle = "#feefde"; 113 ctx.fill(); 114 ctx.strokeStyle = "#6e6d6e"; 115 ctx.lineWidth = radius * 0.1; 116 ctx.stroke(); 117 ctx.restore(); 118 } 119 120 function markHours(ctx, radius) { 121 ctx.save(); 122 ctx.strokeStyle = "#947360"; 123 ctx.lineWidth = radius * 0.05; 124 for (let i = 0; i < 12; i++) { 125 ctx.beginPath(); 126 ctx.rotate(Math.PI / 6); 127 ctx.moveTo(radius * 0.7, 0); 128 ctx.lineTo(radius * 0.9, 0); 129 ctx.stroke(); 130 } 131 ctx.restore(); 132 } 133 134 function markMinutes(ctx, radius) { 135 ctx.save(); 136 ctx.strokeStyle = "#947360"; 137 ctx.lineWidth = radius * 0.01; 138 for (let i = 0; i < 60; i++) { 139 if (i % 5 !== 0) { 140 ctx.beginPath(); 141 ctx.moveTo(radius * 0.8, 0); 142 ctx.lineTo(radius * 0.85, 0); 143 ctx.stroke(); 144 } 145 ctx.rotate(Math.PI / 30); 146 } 147 ctx.restore(); 148 } 149 150 function drawTime(ctx, radius) { 151 ctx.save(); 152 const now = new Date(); 153 let hour = now.getHours(); 154 let minute = now.getMinutes(); 155 let second = now.getSeconds() + now.getMilliseconds() / 1000; 156 157 hour = hour % 12; 158 hour = 159 (hour * Math.PI) / 6 + 160 (minute * Math.PI) / (6 * 60) + 161 (second * Math.PI) / (360 * 60); 162 drawHand(ctx, hour, radius * 0.5, radius * 0.07, "#a1afa0"); 163 164 minute = (minute * Math.PI) / 30 + (second * Math.PI) / (30 * 60); 165 drawHand(ctx, minute, radius * 0.8, radius * 0.07, "#a1afa0"); 166 167 second = (second * Math.PI) / 30; 168 drawHand(ctx, second, radius * 0.9, radius * 0.02, "#970c10"); 169 ctx.restore(); 170 } 171 172 function drawHand(ctx, pos, length, width, color = "black") { 173 ctx.save(); 174 ctx.strokeStyle = color; 175 ctx.beginPath(); 176 ctx.lineWidth = width; 177 ctx.lineCap = "round"; 178 ctx.moveTo(0, 0); 179 ctx.rotate(pos); 180 ctx.lineTo(0, -length); 181 ctx.stroke(); 182 ctx.rotate(-pos); 183 ctx.restore(); 184 } 185 186 function configureEncoder( 187 worker, 188 width, 189 height, 190 codec, 191 latencyMode, 192 avcFormat 193 ) { 194 worker.postMessage({ 195 command: "configure", 196 codec, 197 width, 198 height, 199 latencyMode, 200 avcFormat, 201 }); 202 } 203 204 async function encodeCanvas( 205 worker, 206 canvas, 207 fps, 208 totalDuration, 209 keyFrameIntervalInFrames 210 ) { 211 const frameDuration = Math.round(1000 / fps); // ms 212 let encodeDuration = 0; 213 let frameCount = 0; 214 let intervalId; 215 216 return new Promise((resolve, _) => { 217 // first callback happens after frameDuration. 218 intervalId = setInterval(() => { 219 if (encodeDuration > totalDuration) { 220 clearInterval(intervalId); 221 resolve(encodeDuration); 222 return; 223 } 224 drawClock(canvas); 225 const frame = new VideoFrame(canvas, { timestamp: encodeDuration }); 226 worker.postMessage({ 227 command: "encode", 228 frame, 229 isKey: frameCount % keyFrameIntervalInFrames == 0, 230 }); 231 frameCount += 1; 232 encodeDuration += frameDuration; 233 }, frameDuration); 234 }); 235 } 236 237 async function getEncoderResults(worker) { 238 worker.postMessage({ command: "flush" }); 239 return new Promise((resolve, _) => { 240 worker.onmessage = event => { 241 if (event.data.command === "result") { 242 const { encodeTimes, outputTimes } = event.data; 243 resolve({ encodeTimes, outputTimes }); 244 } 245 }; 246 }); 247 } 248 249 function getTotalDuration(encodeTimes, outputTimes) { 250 if (!outputTimes.length || encodeTimes.length < outputTimes.length) { 251 return Infinity; 252 } 253 return outputTimes[outputTimes.length - 1].time - encodeTimes[0].time; 254 } 255 256 function calculateRoundTripTimes(encodeTimes, outputTimes) { 257 let roundTripTimes = []; 258 let encodeIndex = 0; 259 let outputIndex = 0; 260 while ( 261 encodeIndex < encodeTimes.length && 262 outputIndex < outputTimes.length 263 ) { 264 const encodeEntry = encodeTimes[encodeIndex]; 265 const outputEntry = outputTimes[outputIndex]; 266 267 if (encodeEntry.timestamp === outputEntry.timestamp) { 268 const roundTripTime = outputEntry.time - encodeEntry.time; 269 roundTripTimes.push({ 270 timestamp: outputEntry.timestamp, 271 time: roundTripTime, 272 }); 273 encodeIndex++; 274 outputIndex++; 275 } else if (encodeEntry.timestamp < outputEntry.timestamp) { 276 encodeIndex++; 277 } else { 278 outputIndex++; 279 } 280 } 281 return roundTripTimes; 282 } 283 284 function getMeanAndStandardDeviation(values) { 285 if (!values.length) { 286 return { mean: 0, stddev: 0 }; 287 } 288 const mean = values.reduce((a, b) => a + b, 0) / values.length; 289 const stddev = Math.sqrt( 290 values.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / 291 values.length 292 ); 293 return { mean, stddev }; 294 } 295 296 function reportMetrics(results) { 297 const metrics = {}; 298 let text = "\nResults (ms)\n"; 299 for (const mode in results) { 300 for (const r of results[mode]) { 301 const name = mode + " - " + r.name; 302 metrics[name] = r.value; 303 text += " " + mode + " " + r.name + " : " + r.value + "\n"; 304 } 305 } 306 dump(text); 307 info("perfMetrics", JSON.stringify(metrics)); 308 } 309 310 add_task(async () => { 311 const width = 640; 312 const height = 480; 313 const fps = 30; 314 const totalDuration = 5000; // ms 315 const keyFrameInterval = 15; // 1 key every 15 frames 316 317 const worker = new Worker("encode_from_canvas.js"); 318 const h264main = "avc1.4D001E"; 319 configureEncoder(worker, width, height, h264main, REALTIME, "annexb"); 320 321 const canvas = createCanvas(width, height); 322 await encodeCanvas(worker, canvas, fps, totalDuration, keyFrameInterval); 323 let { encodeTimes, outputTimes } = await getEncoderResults(worker); 324 325 ok( 326 encodeTimes.length >= outputTimes.length, 327 "Should have more encoded samples than outputs" 328 ); 329 330 let results = { key: {}, delta: {} }; 331 results.key.encodeTimes = encodeTimes.filter(x => x.type == "key"); 332 results.delta.encodeTimes = encodeTimes.filter(x => x.type != "key"); 333 results.key.outputTimes = outputTimes.filter(x => x.type == "key"); 334 results.delta.outputTimes = outputTimes.filter(x => x.type != "key"); 335 ok( 336 results.key.encodeTimes.length >= results.key.outputTimes.length, 337 "Should have more encoded samples than outputs (key)" 338 ); 339 ok( 340 results.delta.encodeTimes.length >= results.delta.outputTimes.length, 341 "Should have more encoded samples than outputs (delta)" 342 ); 343 344 results.key.frameDroppingRate = 345 (results.key.encodeTimes.length - results.key.outputTimes.length) / 346 results.key.encodeTimes.length; 347 results.delta.frameDroppingRate = 348 (results.delta.encodeTimes.length - results.delta.outputTimes.length) / 349 results.delta.encodeTimes.length; 350 351 results.key.roundTripTimes = calculateRoundTripTimes( 352 results.key.encodeTimes, 353 results.key.outputTimes 354 ); 355 results.key.roundTripResult = getMeanAndStandardDeviation( 356 results.key.roundTripTimes.map(x => x.time) 357 ); 358 359 results.delta.roundTripTimes = calculateRoundTripTimes( 360 results.delta.encodeTimes, 361 results.delta.outputTimes 362 ); 363 results.delta.roundTripResult = getMeanAndStandardDeviation( 364 results.delta.roundTripTimes.map(x => x.time) 365 ); 366 367 RESULTS[REALTIME][0].value = results.key.roundTripResult.mean; 368 RESULTS[REALTIME][1].value = results.key.roundTripResult.stddev; 369 RESULTS[REALTIME][2].value = results.key.frameDroppingRate; 370 371 RESULTS[REALTIME][3].value = results.delta.roundTripResult.mean; 372 RESULTS[REALTIME][4].value = results.delta.roundTripResult.stddev; 373 RESULTS[REALTIME][5].value = results.delta.frameDroppingRate; 374 375 removeCanvas(canvas); 376 worker.terminate(); 377 }); 378 379 add_task(async () => { 380 const width = 640; 381 const height = 480; 382 const fps = 30; 383 const totalDuration = 5000; // ms 384 const keyFrameInterval = 15; // 1 key every 15 frames 385 386 const worker = new Worker("encode_from_canvas.js"); 387 const h264main = "avc1.4D001E"; 388 configureEncoder(worker, width, height, h264main, QUALITY, "annexb"); 389 390 const canvas = createCanvas(width, height); 391 await encodeCanvas(worker, canvas, fps, totalDuration, keyFrameInterval); 392 let { encodeTimes, outputTimes } = await getEncoderResults(worker); 393 394 is( 395 encodeTimes.length, 396 outputTimes.length, 397 `frame cannot be dropped in ${QUALITY} mode` 398 ); 399 RESULTS[QUALITY][0].value = getTotalDuration(encodeTimes, outputTimes); 400 401 removeCanvas(canvas); 402 worker.terminate(); 403 }); 404 405 add_task(async () => { 406 reportMetrics(RESULTS); 407 }); 408 </script> 409 <body></body> 410 </html>