tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>