vdo.ninja/speedtest.html
2022-10-27 08:22:45 -04:00

571 lines
18 KiB
HTML

<html>
<head>
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<link rel="stylesheet" href="./speedtest.css?ver=1" />
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VDON Speed Test</title>
</head>
<body onload="loadIframe();">
<h1>
VDO.Ninja's video streaming quality test
</h1>
<div id="container">
</div>
<div id="graphs">
<div class="graph">
<h2>Bitrate (kbps)</h2>
<span>0</span>
<canvas id="bitrate-graph"></canvas>
</div>
<div class="graph">
<h2>Buffer delay (ms)</h2>
<span>0</span>
<canvas id="buffer-graph"></canvas>
</div>
<div class="graph">
<h2>Packet Loss (%)</h2>
<span>0</span>
<canvas id="packetloss-graph"></canvas>
</div>
</div>
<div id="log" onclick="copyFunction(this.innerText)">
<h2>Log <i class="las la-clipboard"></i></h2>
<ul></ul>
</div>
<div id="explanation">
<h2>How to use</h2>
<ol>
<li>Select your camera.</li>
<li>Hit start</li>
<li>
Wait for the video to load side-by-side. *If it does not auto-load within 20s, refresh and try again.*
</li>
<li>
Stats will be printed to screen, along with graphs visualizing the most important metrics. You can also hold CTRL (cmd) + Click on the video to access even more stats.
</li>
<li>
Bitrate, Buffer delay, and packet loss are important connection quality metrics. Monitor them for at least a minute.
</li>
<li>
Change the video bitrate by pressing the buttons below the video. It should approach 6000-kbps if the network allows.
</li>
<li>
Good packet loss will not exceed 0.1% and bad packet loss will exceed 1%. You can try testing against different global regions with the drop-down menu below.
</li>
<li>
Check out <a href="https://youtu.be/je2ljlvLzlY" target="_blank" style='color:#DEF;'>this Youtube video</a> for more details and solutions to reduce packet loss if suffering from any.
</li>
</ol>
<h2>Unsupervised guest check option</h2>
<span style="margin:5px;line-height:32px;font-size:110%;">
You can also use <a target="_blank" style="color: #CCC;" href='./check.html'>this tool</a> to have a guest perform a system and connection test, with those results being available to you via a result's page for up to a week.<br />
You can either ask the remote guest to send you the link to their results page when they complete the test, or you can use <i style="color: #CCF;" >check.html?id=xxx</i> to pre-assign the test ID (ie: xxx), which will have the results then be available at <i style="color: #CCF;" >results.html?id=xxx</i>.
<br />If you have questions, <a href="https://discord.vdo.ninja" style="color: #CCC;" target="_blank">join the Discord here</a>.
</span>
<br />
<h3>More testing options below</h3>
<div id="screen" style="color: #CCC; margin:10px 0;"><a href="./speedtest?screen" style="color: #CCC;">Test screen-sharing performance here</a></div>
<div id="remote"></div>
<br />
Testing location: <select name="turnlist" id="turnlist" onchange="reloadTurn();" title="Select an exact location to test against">
<option selected value="">Automatic</option>
<option value="de1">Saarbruecken, Germany</option>
<option value="de2">Frankfurt, Germany</option>
<option value="fr1">Strasbourg, France</option>
<option value="bra1">São Paulo, Brazil</option>
<option value="pol1">Warsaw, Poland</option>
<option value="cae1">Montreal, Canada</option>
<option value="use1">Virgina, USA</option>
<option disabled value="usc1">Chicago, USA</option>
<option disabled value="usw1">Los Angeles, USA</option>
<option value="usw2">Oregon, USA</option>
<option value="aus1">Sydney, Australia</option>
<option value="jap1">Tokyo, Japan</option>
<option value="sing1">Singapore</option>
<option value="ind1">Mumbai, India</option>
<option value="pol1">Warsaw, Poland</option>
</select>
<br /><br /><br />
</div>
<script>
function getChromeVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
if (!getChromeVersion()){
alert("This speedtest is optimized for Chromium-based browsers; graphs will not work for Firefox or Safari browsers.");
}
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(
self.searchString
);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
var quality_reason = "";
var encoder = "";
var Round_Trip_Time_ms = "";
var recordResults = false;
function copyFunction(copyText) {
alert("Log copied to the clipboard.");
try {
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
} catch (e) {
var dummy = document.createElement("input");
document.body.appendChild(dummy);
dummy.value = copyText;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
return false;
}
}
function printValues(obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out += "<br />";
out += printValues(obj[key]);
} else {
if (key.startsWith("_")) {
} else {
out += "<b>" + key + "</b>: " + obj[key] + "<br />";
}
}
}
return out;
}
function logData(type, data) {
var log = document.getElementById("log").getElementsByTagName("ul")[0];
var entry = document.createElement('li');
entry.textContent =
"[" + new Date().toLocaleTimeString() + "] " + type + " : " + data;
log.prepend(entry);
}
function reloadTurn(){
console.log("Reloading to change TURN servers");
loadIframe(document.getElementById("turnlist").value);
}
function updateTurnlist(value){
var select = document.getElementById("turnlist");
var selected = select.value;
select.innerHTML = "";
var opt = document.createElement("option");
opt.value = ""
opt.title = "Choose the closest location automatically";
opt.innerHTML = "Automatic";
select.appendChild(opt);
if (selected == ""){
opt.selected = true;
}
for (var i =0;i<value.length;i++){
var opt = document.createElement("option");
opt.value = value[i].locale;
opt.title = value[i].name;
opt.innerHTML = value[i].name;
select.appendChild(opt);
if (selected == opt.value){
opt.selected = true;
}
}
}
function loadIframe(zone="") {
// this is pretty important if you want to avoid camera permission popup problems. YOu need to load the iFRAME after you load the parent body. A quick solution is like: <body onload=>loadIframe();"> !!!
document.getElementById("container").innerHTML = "";
var streamID = "";
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
for (var i = 0; i < 7; i++) {
streamID += possible.charAt(
Math.floor(Math.random() * possible.length)
);
}
if (urlParams.has("sid")) {
streamID = urlParams.get("sid");
}
document.getElementById("remote").innerHTML = "<a style='color:#CCC' target='_blank' href='./monitor?sid="+streamID+"'>Remote Monitoring Link</a><br />";
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("span");
iframe.allow="autoplay;camera;microphone;display-capture;";
iframe.allowtransparency="true";
iframe.allowfullscreen ="true";
//iframe.allow = "autoplay";
var srcString = "./?push=" + streamID + "&cleanoutput&privacy&"+testType+"&audiodevice=0&fullscreen&transparent&remote&speedtest="+zone;
if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
}
// we are changing some text on page load, just to demonstrate what's possible.
iframe.onload = function (e) {
e.target.contentWindow.postMessage(
{
function: "changeHTML",
target: "add_camera",
value: "Select your Camera",
},
"*"
);
};
iframe.src = srcString;
iframeContainer.appendChild(iframe);
var title = document.createElement("h3");
title.innerText = "Local video feed";
iframeContainer.appendChild(title);
var feeds = document.createElement("div");
feeds.id = "feeds";
document.getElementById("container").appendChild(feeds);
document.getElementById("feeds").appendChild(iframeContainer);
setInterval(function (iframe1) {
try {
iframe1.contentWindow.postMessage({ getStats: true }, "*");
} catch(e){
clearInterval(this);
}
}, 1000, iframe);
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("span");
iframe.allow = "autoplay";
var srcString = "./?view=" + streamID + "&cleanoutput&privacy&noaudio&scale=0&speedtest="+zone; // No TURN servers set on the reciever. Don't want to query for TURN servers needlessly.
if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
}
if (urlParams.has("buffer")) {
srcString = srcString + "&buffer=" + urlParams.get("buffer");
}
if (urlParams.has("record")) {
recordResults = true;
}
iframe.src = srcString;
iframeContainer.appendChild(iframe);
var title = document.createElement("h3");
title.innerText = "Server video feed";
iframeContainer.appendChild(title);
document.getElementById("feeds").appendChild(iframeContainer);
var button = document.createElement("br");
document.getElementById("container").appendChild(button);
var buttonContainer = document.createElement("div");
buttonContainer.id = "controls";
var button = document.createElement("button");
button.innerHTML = "Low Bitrate";
button.className = "grey";
button.onclick = function () {
iframe.contentWindow.postMessage({ bitrate: 30 }, "*");
bitrate.target = 30;
};
buttonContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "High Bitrate";
button.className = "grey";
button.onclick = function () {
iframe.contentWindow.postMessage({ bitrate: 6000 }, "*");
bitrate.target = 6000;
};
buttonContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Default Bitrate";
button.className = "grey active";
button.onclick = function () {
iframe.contentWindow.postMessage({ bitrate: -1 }, "*");
bitrate.target = 3000;
};
buttonContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Disconnect";
button.className = "red";
button.style.display = "none";
button.onclick = function () {
iframe.contentWindow.postMessage({ close: true }, "*");
};
buttonContainer.appendChild(button);
document.getElementById("container").appendChild(buttonContainer);
setInterval(function (iframe1) {
try {
iframe1.contentWindow.postMessage({ getStats: true }, "*");
} catch(e){
clearInterval(this);
}
}, 1000, iframe);
var eventMethod = window.addEventListener
? "addEventListener"
: "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
var previousResolution;
eventer(messageEvent, function (e) {
//console.log(e.data);
if ("action" in e.data) {
if (e.data.action == "available-speedtest-servers"){
console.warn("Speedtest server list loaded");
updateTurnlist(e.data.value);
} else {
logData(e.data.action, e.data.value);
}
if (e.data.action == "new-view-connection") {
buttonContainer.querySelectorAll(
"#controls button:last-child"
)[0].style.display = "inline";
}
if (e.data.action == "setVideoBitrate") {
buttonContainer.querySelectorAll("button").forEach((button) => {
button.classList.remove("active");
});
if (e.data.value == 30) {
document
.querySelectorAll("#controls button")[0]
.classList.add("active");
}
if (e.data.value == 6000) {
document
.querySelectorAll("#controls button")[1]
.classList.add("active");
}
if (e.data.value == -1) {
document
.querySelectorAll("#controls button")[2]
.classList.add("active");
}
}
}
if ("stats" in e.data) {
var out = "";
for (var streamID in e.data.stats.inbound_stats) {
out += printValues(e.data.stats.inbound_stats[streamID]);
}
for (var streamID in e.data.stats.outbound_stats) {
if (e.data.stats.outbound_stats[streamID].quality_limitation_reason){
if (quality_reason != e.data.stats.outbound_stats[streamID].quality_limitation_reason) {
quality_reason = e.data.stats.outbound_stats[streamID].quality_limitation_reason;
logData("Quality Limitation Reason:", quality_reason);
}
}
if (e.data.stats.outbound_stats[streamID].encoder){
if (encoder != e.data.stats.outbound_stats[streamID].encoder) {
encoder = e.data.stats.outbound_stats[streamID].encoder;
logData("Encoder used:", encoder);
}
}
}
if (out.split("Bitrate_in_kbps").length > 1) {
for (var key in e.data.stats.inbound_stats[streamID]) {
if (key.startsWith("RTCMediaStreamTrack_receiver") || key.startsWith("DEPRECATED_RTCMediaStreamTrack_receiver")) {
var bitrate = e.data.stats.inbound_stats[streamID][key][
"Bitrate_in_kbps"
];
updateData("bitrate", bitrate);
var buffer = e.data.stats.inbound_stats[streamID][key][
"Buffer_Delay_in_ms"
];
updateData("buffer", buffer);
var packetloss = e.data.stats.inbound_stats[streamID][key]["packetLoss_in_percentage"];
if (packetloss != undefined) {
packetloss = packetloss.toFixed(2);
updateData("packetloss", packetloss);
}
var resolution = e.data.stats.inbound_stats[streamID][key]["Resolution"];
if (previousResolution != resolution) {
previousResolution = resolution;
logData("Resolution", resolution);
}
}
}
}
if (recordResults){
var request = new XMLHttpRequest();
request.open('POST', "https://reports.vdo.ninja/record");
request.send(JSON.stringify(e.data.stats));
}
}
});
}
var testType= "webcam&css=speedtest.css";
if (urlParams.has("screen") || urlParams.has("ss") || urlParams.has("screenshare") || urlParams.has("screentest")) {
document.getElementById("screen").innerHTML = '<a href="./speedtest" style="color: #CCC;">Test webcam-streaming performance here</a>';
testType = "quality=0&screenshare&css=speedtest.css"
}
var bitrate = {
element: "bitrate-graph",
data: 0,
max: 6000,
target: 3000,
};
var frames;
var buffer = {
element: "buffer-graph",
data: 0,
max: 200,
target: 100,
};
var packetloss = {
element: "packetloss-graph",
data: 0,
max: 3,
target: 2,
};
function updateData(type, data) {
if (type == "bitrate") {
bitrate.data = data;
plotData("bitrate", bitrate);
}
if (type == "buffer") {
buffer.data = data;
plotData("buffer", buffer);
}
if (type == "packetloss") {
packetloss.data = data;
plotData("packetloss", packetloss);
}
}
function plotData(type, stat) {
var canvas;
var context;
var yScale;
canvas = document.getElementById(stat.element);
context = canvas.getContext("2d");
if (isNaN(stat.data)) {
stat.data = 0;
}
var text = (canvas.previousElementSibling.innerHTML = stat.data);
var height = context.canvas.height;
var width = context.canvas.width;
var borderWidth = 5;
var offset = borderWidth * 2;
// Create gradient
var grd = context.createLinearGradient(0, 0, 0, height);
if (type == "bitrate") {
// Higher values are green
grd.addColorStop(0, "#33C433");
grd.addColorStop(0.7, "#F3F304");
grd.addColorStop(0.9, "#F30404");
} else {
// Higher values are red
grd.addColorStop(0, "#F30404");
grd.addColorStop(0.3, "#F3F304");
grd.addColorStop(0.7, "#33C433");
}
context.strokeStyle = "white";
context.fillStyle = grd;
//context.fillStyle = "#009933";
//context.imageSmoothingEnabled = true;
yScale = height / stat.target;
if (stat.data > stat.target) {
stat.data = stat.target;
}
if (type == "packetloss" && stat.data == 0.0) {
stat.data = 0.1;
}
var x = width - 1;
var y = height - stat.data * yScale;
var w = 1;
context.fillStyle = grd;
context.fillRect(x, y, w, height);
// shift everything to the left:
var imageData = context.getImageData(1, 0, width - 1, height);
context.putImageData(imageData, 0, 0);
// now clear the right-most pixels:
context.clearRect(width - 1, 0, 1, height);
}
</script>
</body>
</html>