mirror of
https://github.com/librespeed/speedtest.git
synced 2026-05-13 16:26:38 +00:00
Agent-Logs-Url: https://github.com/librespeed/speedtest/sessions/cfb05653-7d75-4f0d-9e9a-1765eeaa8903 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>
590 lines
16 KiB
HTML
590 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<link rel="shortcut icon" href="favicon.ico">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
|
|
<meta charset="UTF-8" />
|
|
<script type="text/javascript" src="speedtest.js"></script>
|
|
<script type="text/javascript">
|
|
function I(i) { return document.getElementById(i); }
|
|
|
|
//LIST OF TEST SERVERS. Leave empty if you're doing a standalone installation. See documentation for details
|
|
var SPEEDTEST_SERVERS = [
|
|
/*{ //this server doesn't actually exist, remove it
|
|
name:"Example Server 1", //user friendly name for the server
|
|
server:"//test1.mydomain.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
|
|
dlURL:"backend/garbage.php", //path to download test on this server (garbage.php or replacement)
|
|
ulURL:"backend/empty.php", //path to upload test on this server (empty.php or replacement)
|
|
pingURL:"backend/empty.php", //path to ping/jitter test on this server (empty.php or replacement)
|
|
getIpURL:"backend/getIP.php" //path to getIP on this server (getIP.php or replacement)
|
|
},
|
|
{ //this server doesn't actually exist, remove it
|
|
name:"Example Server 2", //user friendly name for the server
|
|
server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
|
|
dlURL:"garbage.php", //path to download test on this server (garbage.php or replacement)
|
|
ulURL:"empty.php", //path to upload test on this server (empty.php or replacement)
|
|
pingURL:"empty.php", //path to ping/jitter test on this server (empty.php or replacement)
|
|
getIpURL:"getIP.php" //path to getIP on this server (getIP.php or replacement)
|
|
}*/
|
|
//add other servers here, comma separated
|
|
];
|
|
|
|
//INITIALIZE SPEEDTEST
|
|
var s = new Speedtest(); //create speed test object
|
|
s.setParameter("telemetry_level", "basic"); //enable basic telemetry (for results sharing)
|
|
|
|
//SERVER AUTO SELECTION
|
|
function initServers() {
|
|
if (SPEEDTEST_SERVERS.length == 0) { //standalone installation
|
|
//just make the UI visible
|
|
I("loading").className = "hidden";
|
|
I("serverArea").style.display = "none";
|
|
I("testWrapper").className = "visible";
|
|
initUI();
|
|
} else { //multiple servers
|
|
var noServersAvailable = function () {
|
|
I("message").innerHTML = "No servers available";
|
|
}
|
|
var runServerSelect = function () {
|
|
s.selectServer(function (server) {
|
|
if (server != null) { //at least 1 server is available
|
|
I("loading").className = "hidden"; //hide loading message
|
|
//populate server list for manual selection
|
|
for (var i = 0; i < SPEEDTEST_SERVERS.length; i++) {
|
|
if (SPEEDTEST_SERVERS[i].pingT == -1) continue;
|
|
var option = document.createElement("option");
|
|
option.value = i;
|
|
option.textContent = SPEEDTEST_SERVERS[i].name;
|
|
if (SPEEDTEST_SERVERS[i] === server) option.selected = true;
|
|
I("server").appendChild(option);
|
|
}
|
|
//show test UI
|
|
I("testWrapper").className = "visible";
|
|
initUI();
|
|
} else { //no servers are available, the test cannot proceed
|
|
noServersAvailable();
|
|
}
|
|
});
|
|
}
|
|
if (typeof SPEEDTEST_SERVERS === "string") {
|
|
//need to fetch list of servers from specified URL
|
|
s.loadServerList(SPEEDTEST_SERVERS, function (servers) {
|
|
if (servers == null) { //failed to load server list
|
|
noServersAvailable();
|
|
} else { //server list loaded
|
|
SPEEDTEST_SERVERS = servers;
|
|
runServerSelect();
|
|
}
|
|
});
|
|
} else {
|
|
//hardcoded server list
|
|
s.addTestPoints(SPEEDTEST_SERVERS);
|
|
runServerSelect();
|
|
}
|
|
}
|
|
}
|
|
|
|
var meterBk = /Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent) ? "#EAEAEA" : "#80808040";
|
|
var dlColor = "#6060AA",
|
|
ulColor = "#616161";
|
|
var progColor = meterBk;
|
|
|
|
//CODE FOR GAUGES
|
|
function drawMeter(c, amount, bk, fg, progress, prog) {
|
|
var ctx = c.getContext("2d");
|
|
var dp = window.devicePixelRatio || 1;
|
|
var cw = c.clientWidth * dp, ch = c.clientHeight * dp;
|
|
var sizScale = ch * 0.0055;
|
|
if (c.width == cw && c.height == ch) {
|
|
ctx.clearRect(0, 0, cw, ch);
|
|
} else {
|
|
c.width = cw;
|
|
c.height = ch;
|
|
}
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = bk;
|
|
ctx.lineWidth = 12 * sizScale;
|
|
ctx.arc(c.width / 2, c.height - 58 * sizScale, c.height / 1.8 - ctx.lineWidth, -Math.PI * 1.1, Math.PI * 0.1);
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = fg;
|
|
ctx.lineWidth = 12 * sizScale;
|
|
ctx.arc(c.width / 2, c.height - 58 * sizScale, c.height / 1.8 - ctx.lineWidth, -Math.PI * 1.1, amount * Math.PI * 1.2 - Math.PI * 1.1);
|
|
ctx.stroke();
|
|
if (typeof progress !== "undefined") {
|
|
ctx.fillStyle = prog;
|
|
ctx.fillRect(c.width * 0.3, c.height - 16 * sizScale, c.width * 0.4 * progress, 4 * sizScale);
|
|
}
|
|
}
|
|
function mbpsToAmount(s) {
|
|
return 1 - (1 / (Math.pow(1.3, Math.sqrt(s))));
|
|
}
|
|
function format(d) {
|
|
d = Number(d);
|
|
if (d < 10) return d.toFixed(2);
|
|
if (d < 100) return d.toFixed(1);
|
|
return d.toFixed(0);
|
|
}
|
|
|
|
//UI CODE
|
|
var uiData = null;
|
|
function startStop() {
|
|
if (s.getState() == 3) {
|
|
//speed test is running, abort
|
|
s.abort();
|
|
data = null;
|
|
I("startStopBtn").className = "";
|
|
I("startStopBtn").setAttribute("aria-label", "Start");
|
|
I("server").disabled = false;
|
|
initUI();
|
|
} else {
|
|
//test is not running, begin
|
|
I("startStopBtn").className = "running";
|
|
I("startStopBtn").setAttribute("aria-label", "Abort");
|
|
I("shareArea").style.display = "none";
|
|
I("server").disabled = true;
|
|
s.onupdate = function (data) {
|
|
uiData = data;
|
|
};
|
|
s.onend = function (aborted) {
|
|
I("startStopBtn").className = "";
|
|
I("startStopBtn").setAttribute("aria-label", "Start");
|
|
I("server").disabled = false;
|
|
updateUI(true);
|
|
if (!aborted) {
|
|
//if testId is present, show sharing panel, otherwise do nothing
|
|
try {
|
|
var testId = uiData.testId;
|
|
if (testId != null) {
|
|
var shareURL = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/results/?id=" + testId;
|
|
I("resultsImg").src = shareURL;
|
|
I("resultsURL").value = shareURL;
|
|
I("testId").innerHTML = testId;
|
|
I("shareArea").style.display = "";
|
|
}
|
|
} catch (e) { }
|
|
}
|
|
};
|
|
s.start();
|
|
}
|
|
}
|
|
//this function reads the data sent back by the test and updates the UI
|
|
function updateUI(forced) {
|
|
if (!forced && s.getState() != 3) return;
|
|
if (uiData == null) return;
|
|
var status = uiData.testState;
|
|
I("ip").textContent = uiData.clientIp;
|
|
I("dlText").textContent = (status == 1 && uiData.dlStatus == 0) ? "..." : format(uiData.dlStatus);
|
|
drawMeter(I("dlMeter"), mbpsToAmount(Number(uiData.dlStatus * (status == 1 ? oscillate() : 1))), meterBk, dlColor, Number(uiData.dlProgress), progColor);
|
|
I("ulText").textContent = (status == 3 && uiData.ulStatus == 0) ? "..." : format(uiData.ulStatus);
|
|
drawMeter(I("ulMeter"), mbpsToAmount(Number(uiData.ulStatus * (status == 3 ? oscillate() : 1))), meterBk, ulColor, Number(uiData.ulProgress), progColor);
|
|
I("pingText").textContent = format(uiData.pingStatus);
|
|
I("jitText").textContent = format(uiData.jitterStatus);
|
|
}
|
|
function oscillate() {
|
|
return 1 + 0.02 * Math.sin(Date.now() / 100);
|
|
}
|
|
//update the UI every frame
|
|
window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || (function (callback, element) { setTimeout(callback, 1000 / 60); });
|
|
function frame() {
|
|
requestAnimationFrame(frame);
|
|
updateUI();
|
|
}
|
|
frame(); //start frame loop
|
|
//function to (re)initialize UI
|
|
function initUI() {
|
|
drawMeter(I("dlMeter"), 0, meterBk, dlColor, 0);
|
|
drawMeter(I("ulMeter"), 0, meterBk, ulColor, 0);
|
|
I("dlText").textContent = "";
|
|
I("ulText").textContent = "";
|
|
I("pingText").textContent = "";
|
|
I("jitText").textContent = "";
|
|
I("ip").textContent = "";
|
|
}
|
|
</script>
|
|
<style type="text/css">
|
|
html,
|
|
body {
|
|
border: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
background: #FFFFFF;
|
|
color: #202020;
|
|
}
|
|
|
|
body {
|
|
text-align: center;
|
|
font-family: "Roboto", sans-serif;
|
|
}
|
|
|
|
h1 {
|
|
color: #404040;
|
|
}
|
|
|
|
#loading {
|
|
background-color: #FFFFFF;
|
|
color: #404040;
|
|
text-align: center;
|
|
}
|
|
|
|
span.loadCircle {
|
|
display: inline-block;
|
|
width: 2em;
|
|
height: 2em;
|
|
vertical-align: middle;
|
|
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAP1BMVEUAAAB2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZyFzwnAAAAFHRSTlMAEvRFvX406baecwbf0casimhSHyiwmqgAAADpSURBVHja7dbJbQMxAENRahnN5lkc//5rDRAkDeRgHszXgACJoKiIiIiIiIiIiIiIiIiIiIj4HHspsrpAVhdVVguzrA4OWc10WcEqpwKbnBo0OU1Q5NSpsoJFTgOecrrdEag85DRgktNqfoEdTjnd7hrEHMEJvmRUYJbTYk5Agy6nau6Abp5Cm7mDBtRdPi9gyKdU7w4p1fsLvyqs8hl4z9/w3n/Hmr9WoQ65lAU4d7lMYOz//QboRR5jBZibLMZdAR6O/Vfa1PlxNr3XdS3HzK/HVPRu/KnLs8iAOh993VpRRERERMT/fAN60wwWaVyWwAAAAABJRU5ErkJggg==');
|
|
background-size: 2em 2em;
|
|
margin-right: 0.5em;
|
|
animation: spin 0.6s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: rotate(359deg);
|
|
}
|
|
}
|
|
|
|
#startStopBtn {
|
|
display: inline-block;
|
|
margin: 0 auto;
|
|
color: #6060AA;
|
|
background-color: rgba(0, 0, 0, 0);
|
|
border: 0.15em solid #6060FF;
|
|
padding: 0;
|
|
font: inherit;
|
|
border-radius: 0.3em;
|
|
transition: all 0.3s;
|
|
box-sizing: border-box;
|
|
width: 8em;
|
|
height: 3em;
|
|
line-height: 2.7em;
|
|
cursor: pointer;
|
|
box-shadow: 0 0 0 rgba(0, 0, 0, 0.1), inset 0 0 0 rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
#startStopBtn:hover {
|
|
box-shadow: 0 0 2em rgba(0, 0, 0, 0.1), inset 0 0 1em rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
#startStopBtn.running {
|
|
background-color: #FF3030;
|
|
border-color: #FF6060;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
#startStopBtn:before {
|
|
content: "Start";
|
|
}
|
|
|
|
#startStopBtn.running:before {
|
|
content: "Abort";
|
|
}
|
|
|
|
#serverArea {
|
|
margin-top: 1em;
|
|
}
|
|
|
|
#server {
|
|
font-size: 1em;
|
|
padding: 0.2em;
|
|
}
|
|
|
|
#test {
|
|
margin-top: 2em;
|
|
margin-bottom: 12em;
|
|
}
|
|
|
|
div.testArea {
|
|
display: inline-block;
|
|
width: 16em;
|
|
height: 12.5em;
|
|
position: relative;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
div.testArea2 {
|
|
display: inline-block;
|
|
width: 14em;
|
|
height: 7em;
|
|
position: relative;
|
|
box-sizing: border-box;
|
|
text-align: center;
|
|
}
|
|
|
|
div.testArea div.testName {
|
|
position: absolute;
|
|
top: 0.1em;
|
|
left: 0;
|
|
width: 100%;
|
|
font-size: 1.4em;
|
|
z-index: 9;
|
|
}
|
|
|
|
div.testArea2 div.testName {
|
|
display: block;
|
|
text-align: center;
|
|
font-size: 1.4em;
|
|
}
|
|
|
|
div.testArea div.meterText {
|
|
position: absolute;
|
|
bottom: 1.55em;
|
|
left: 0;
|
|
width: 100%;
|
|
font-size: 2.5em;
|
|
z-index: 9;
|
|
}
|
|
|
|
div.testArea2 div.meterText {
|
|
display: inline-block;
|
|
font-size: 2.5em;
|
|
}
|
|
|
|
div.meterText:empty:before {
|
|
content: "0.00";
|
|
}
|
|
|
|
div.testArea div.unit {
|
|
position: absolute;
|
|
bottom: 2em;
|
|
left: 0;
|
|
width: 100%;
|
|
z-index: 9;
|
|
}
|
|
|
|
div.testArea2 div.unit {
|
|
display: inline-block;
|
|
}
|
|
|
|
div.testArea canvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 1;
|
|
}
|
|
|
|
div.testGroup {
|
|
display: block;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
#shareArea {
|
|
width: 95%;
|
|
max-width: 40em;
|
|
margin: 0 auto;
|
|
margin-top: 2em;
|
|
}
|
|
|
|
#shareArea>* {
|
|
display: block;
|
|
width: 100%;
|
|
height: auto;
|
|
margin: 0.25em 0;
|
|
}
|
|
|
|
#privacyPolicy {
|
|
position: fixed;
|
|
top: 2em;
|
|
bottom: 2em;
|
|
left: 2em;
|
|
right: 2em;
|
|
overflow-y: auto;
|
|
width: auto;
|
|
height: auto;
|
|
box-shadow: 0 0 3em 1em #000000;
|
|
z-index: 999999;
|
|
text-align: left;
|
|
background-color: #FFFFFF;
|
|
padding: 1em;
|
|
}
|
|
|
|
a.privacy {
|
|
text-align: center;
|
|
font-size: 0.8em;
|
|
color: #808080;
|
|
padding: 0 3em;
|
|
}
|
|
|
|
div.closePrivacyPolicy {
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
div.closePrivacyPolicy a.privacy {
|
|
padding: 1em 3em;
|
|
}
|
|
|
|
@media all and (max-width:40em) {
|
|
body {
|
|
font-size: 0.8em;
|
|
}
|
|
}
|
|
|
|
div.visible {
|
|
animation: fadeIn 0.4s;
|
|
display: block;
|
|
}
|
|
|
|
div.hidden {
|
|
animation: fadeOut 0.4s;
|
|
display: none;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
0% {
|
|
opacity: 0;
|
|
}
|
|
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes fadeOut {
|
|
0% {
|
|
display: block;
|
|
opacity: 1;
|
|
}
|
|
|
|
100% {
|
|
display: block;
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@media all and (prefers-color-scheme: dark) {
|
|
|
|
html,
|
|
body,
|
|
#loading {
|
|
background: #202020;
|
|
color: #F4F4F4;
|
|
color-scheme: dark;
|
|
}
|
|
|
|
h1 {
|
|
color: #E0E0E0;
|
|
}
|
|
|
|
a {
|
|
color: #9090FF;
|
|
}
|
|
|
|
#privacyPolicy {
|
|
background: #000000;
|
|
}
|
|
|
|
#resultsImg {
|
|
filter: invert(1);
|
|
}
|
|
}
|
|
</style>
|
|
<title>LibreSpeed</title>
|
|
</head>
|
|
|
|
<body onload="initServers()">
|
|
<h1>LibreSpeed</h1>
|
|
<div id="loading" class="visible">
|
|
<p id="message"><span class="loadCircle"></span>Selecting a server...</p>
|
|
</div>
|
|
<div id="testWrapper" class="hidden">
|
|
<button id="startStopBtn" onclick="startStop()" aria-label="Start"></button><br />
|
|
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
|
|
<div id="serverArea">
|
|
Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select>
|
|
</div>
|
|
<div id="test">
|
|
<div class="testGroup">
|
|
<div class="testArea2">
|
|
<div class="testName">Ping</div>
|
|
<div id="pingText" class="meterText" style="color:#AA6060"></div>
|
|
<div class="unit">ms</div>
|
|
</div>
|
|
<div class="testArea2">
|
|
<div class="testName">Jitter</div>
|
|
<div id="jitText" class="meterText" style="color:#AA6060"></div>
|
|
<div class="unit">ms</div>
|
|
</div>
|
|
</div>
|
|
<div class="testGroup">
|
|
<div class="testArea">
|
|
<div class="testName">Download</div>
|
|
<canvas id="dlMeter" class="meter"></canvas>
|
|
<div id="dlText" class="meterText"></div>
|
|
<div class="unit">Mbit/s</div>
|
|
</div>
|
|
<div class="testArea">
|
|
<div class="testName">Upload</div>
|
|
<canvas id="ulMeter" class="meter"></canvas>
|
|
<div id="ulText" class="meterText"></div>
|
|
<div class="unit">Mbit/s</div>
|
|
</div>
|
|
</div>
|
|
<div id="ipArea">
|
|
<span id="ip"></span>
|
|
</div>
|
|
<div id="shareArea" style="display:none">
|
|
<h3>Share results</h3>
|
|
<p>Test ID: <span id="testId"></span></p>
|
|
<input type="text" value="" id="resultsURL" readonly="readonly"
|
|
onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')" />
|
|
<img src="" id="resultsImg" />
|
|
</div>
|
|
</div>
|
|
<a href="index.html?design=new">Try the modern design</a><br>
|
|
<a href="https://github.com/librespeed/speedtest">Source code</a>
|
|
</div>
|
|
<div id="privacyPolicy" style="display:none">
|
|
<h2>Privacy Policy</h2>
|
|
<p>This HTML5 speed test server is configured with telemetry enabled.</p>
|
|
<h4>What data we collect</h4>
|
|
<p>
|
|
At the end of the test, the following data is collected and stored:
|
|
<ul>
|
|
<li>Test ID</li>
|
|
<li>Time of testing</li>
|
|
<li>Test results (download and upload speed, ping and jitter)</li>
|
|
<li>IP address</li>
|
|
<li>ISP information</li>
|
|
<li>Approximate location (inferred from IP address, not GPS)</li>
|
|
<li>User agent and browser locale</li>
|
|
<li>Test log (contains no personal information)</li>
|
|
</ul>
|
|
</p>
|
|
<h4>How we use the data</h4>
|
|
<p>
|
|
Data collected through this service is used to:
|
|
<ul>
|
|
<li>Allow sharing of test results (sharable image for forums, etc.)</li>
|
|
<li>To improve the service offered to you (for instance, to detect problems on our side)</li>
|
|
</ul>
|
|
No personal information is disclosed to third parties.
|
|
</p>
|
|
<h4>Your consent</h4>
|
|
<p>
|
|
By starting the test, you consent to the terms of this privacy policy.
|
|
</p>
|
|
<h4>Data removal</h4>
|
|
<p>
|
|
If you want to have your information deleted, you need to provide either the ID of the test or your IP
|
|
address. This is the only way to identify your data, without this information we won't be able to comply
|
|
with your request.<br /><br />
|
|
Contact this email address for all deletion requests:
|
|
<a href="mailto:PUT@YOUR_EMAIL.HERE">TO BE FILLED BY DEVELOPER</a>.
|
|
</p>
|
|
<br /><br />
|
|
<div class="closePrivacyPolicy">
|
|
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a>
|
|
</div>
|
|
<br />
|
|
</div>
|
|
</body>
|
|
|
|
</html>
|