chromium/content/test/data/gpu/vc/test_video_call_fps.html

<!--
Copyright 2024 The Chromium Authors
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.

To change the number of videos, use rows and columns.
Usage: test_video_call_fps.html?rows=2&columns=7

With 1 video, the frame rate should be kept at the monitor refresh rate.
With 4 videos which is assumed to be the video conference mode, the frame rate
should drop and stay at 30 FPS after a short period of time.
However, when we run the default test with 4 videos, We see the frame rate jumps
after it drops to 30 FPS. The reasons for the frame rate to go up to the
monitor refresh rate: (1) user interactions (mouse/keyboard inputs). (2) The
restart of a video. The teddy bear videos used in the test are quite short, so
it restarts quite often in the looping mode.
-->
<html>

<head>
  <style>
    button {
      position: absolute;
      padding: 5px;
      left: 850px;
      top: 2px;
      width: 100px;
    }

  </style>

  <title>Test video call frame rate</title>

  <header style="display: inline">
    <pre id="fps"> Current: -- FPS, 60-Frame-Average: -- FPS, Average Frame Rate: -- FPS, Harmonic Frame Rate: -- FPS </pre>
  </header>

  <script>
    const _defaultRows = 2;
    const _defaultColumns = 2;
    const _totalVideoWidth = 1280;
    const _totalVideoHeight = 720;

    const parsedString = (function (names) {
      const pairs = {};
      for (let i = 0; i < names.length; ++i) {
        const keyValue = names[i].split('=', 2);
        if (keyValue.length === 1) {
          pairs[keyValue[0]] = '';
        } else {
          pairs[keyValue[0]] = decodeURIComponent(keyValue[1].replace(/\+/g, ' '));
        }
      }
      return pairs;
    })(window.location.search.substr(1).split('&'));

    function GetVideoSource(index) {
        let i = index % 3;

        if (i == 0) {
            return './teddy3_vp9_320x180_30fps.webm';
        } else if (i == 1) {
            return './teddy2_vp9_320x180_15fps.webm';
        } else {
           return './teddy1_vp9_320x180_7fps.webm';
        }
    }

    // To restart the calculation for the average FPS and the harmonic FPS on click.
    let restartFPS = 0;
    function handleClick() {
      restartFPS = 1;
    }

    function startVideos() {
      const container = document.getElementById('container');

      // Get the video row count and the column count from the string.
      // Example: videos_mxn.html?rows=9&columns=9
      let videoRows = parsedString['rows'];
      let videoColumns = parsedString['columns'];
      if (videoRows === undefined) {
        videoRows = _defaultRows;
      }
      if (videoColumns === undefined) {
        videoColumns = _defaultColumns;
      }

      const maxColRow = Math.max(videoRows, videoColumns);

      // Calculate the video onscreen size
      const videoWidth = _totalVideoWidth / maxColRow;
      const videoHeight = _totalVideoHeight / maxColRow;

      // Create MxN videos.
      const videoCount = videoRows * videoColumns;
      for (let row = 0; row < videoRows; row++) {
        for (let column = 0; column < videoColumns; column++) {
          // Onscreen position.
          const videoTop = row * videoHeight + 100;
          const videoLeft = column * videoWidth;

          // Video source.
          const i = row * videoColumns + column;
          const videoSrc = GetVideoSource(i);

          createOneVideo(videoTop, videoLeft, videoWidth, videoHeight,
            videoSrc);
        }
      }

    function createOneVideo(top, left, width, height, videoSrc) {
      const video = document.createElement('video');
      video.loop = true;
      video.autoplay = true;
      video.muted = true;
      video.src = videoSrc;
      video.width = width;
      video.height = height;
      video.style.position = "absolute";
      video.style.top = top;
      video.style.left = left;

      video.play();

      container.appendChild(video);
    }

    // Display and update the FPSs on screen every 200 milliseconds.
    const displayInterval = 200;
    let fps = 0;
    let averageFpsOver60Frames = 0;
    let averageFps = 0;
    let harmonicFps = 0;
    const fpsDisplay = document.getElementById('fps').firstChild;

    setInterval(() => {
      fpsDisplay.nodeValue = ` Current: ${fps | 0} FPS, 60-Frame-Average: ${averageFpsOver60Frames | 0} FPS, Average Frame Rate: ${averageFps | 0} FPS, Harmonic Frame Rate: ${harmonicFps | 0} FPS`;
    }, displayInterval);


    // For the FPS calculation.
    let lastTimestamp = performance.now();
    let delaySunOver60Frames = 0;
    let delaySum = 0;
    let delaySquaredSum = 0;
    let frames = 0;

    function animation() {
      // Restart the calculation on click.
      if (restartFPS) {
        delaySunOver60Frames = 0;
        delaySum = 0;
        delaySquaredSum = 0;
        frames = 0;
        restartFPS = 0;
      }

      frames++;
      let now = performance.now();
      let delay = now - lastTimestamp; // In milliseconds.
      delaySum += delay;

      // Current FPS
      fps = 1 / delay * 1000;

      // Average FPS over the period of 1 second.
      delaySunOver60Frames += delay;
      if (frames % 30 == 0) {
        averageFpsOver60Frames = 30 / delaySunOver60Frames * 1000;
        delaySunOver60Frames = 0;
      }

      // Average FPS since start.
      averageFps = frames / delaySum * 1000;

      // Harmonic FPS since start.
      delaySquaredSum += delay * delay;
      harmonicFps = delaySum / delaySquaredSum * 1000;

      lastTimestamp = now;
      requestAnimationFrame(animation);
    }

    requestAnimationFrame(animation);
   }

  </script>
</head>

<body onload = startVideos()>
  <div id="container" style="position:absolute; top:0px; left:0px"></div>
  <button onclick="handleClick()">Restart FPS</button>
</body>

</html>