<!--
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>