mirror of
https://github.com/eliasstepanik/vdo.ninja.git
synced 2026-01-11 21:58:35 +00:00
graphs target value graphs max value graphs now have colors, green / yellow / red log click to copy bitrate buttons reflect incoming connection requested bitrates via background color disconnect button hidden until first connection established
452 lines
12 KiB
HTML
452 lines
12 KiB
HTML
<html>
|
|
<head>
|
|
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
|
|
<link rel="stylesheet" href="./main.css?ver=11" />
|
|
<link rel="stylesheet" href="./speedtest.css?ver=1" />
|
|
<meta charset="utf8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>OBSN Speed Test</title>
|
|
<script>
|
|
(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);
|
|
|
|
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 loadIframe() {
|
|
// 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();"> !!!
|
|
|
|
var streamID = "";
|
|
var possible =
|
|
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
|
for (var i = 0; i < 7; i++) {
|
|
streamID += possible.charAt(
|
|
Math.floor(Math.random() * possible.length)
|
|
);
|
|
}
|
|
|
|
var iframe = document.createElement("iframe");
|
|
var iframeContainer = document.createElement("span");
|
|
|
|
iframe.allow = "autoplay";
|
|
var srcString =
|
|
"./?push=" +
|
|
streamID +
|
|
"&cleanoutput&privacy&webcam&audiodevice=0&fullscreen";
|
|
|
|
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);
|
|
|
|
var iframe = document.createElement("iframe");
|
|
var iframeContainer = document.createElement("span");
|
|
|
|
iframe.allow = "autoplay";
|
|
var srcString = "./?view=" + streamID + "&cleanoutput&privacy&noaudio";
|
|
|
|
if (urlParams.has("turn")) {
|
|
srcString = srcString + "&turn=" + urlParams.get("turn");
|
|
}
|
|
|
|
if (urlParams.has("buffer")) {
|
|
srcString = srcString + "&buffer=" + urlParams.get("buffer");
|
|
}
|
|
|
|
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 () {
|
|
iframe.contentWindow.postMessage({ getStats: true }, "*");
|
|
}, 1000);
|
|
|
|
var eventMethod = window.addEventListener
|
|
? "addEventListener"
|
|
: "attachEvent";
|
|
var eventer = window[eventMethod];
|
|
var messageEvent =
|
|
eventMethod === "attachEvent" ? "onmessage" : "message";
|
|
var previousResolution;
|
|
|
|
eventer(messageEvent, function (e) {
|
|
if ("action" in e.data) {
|
|
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]);
|
|
}
|
|
|
|
if (out.split("Bitrate_in_kbps").length > 1) {
|
|
for (var key in e.data.stats.inbound_stats[streamID]) {
|
|
if (key.startsWith("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);
|
|
}
|
|
}
|
|
}
|
|
|
|
document.getElementById("statsdiv").innerHTML =
|
|
"<b>Bitrate (Kbps)</b>" + out.split("Bitrate_in_kbps")[1];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
</script>
|
|
</head>
|
|
<body onload="loadIframe();">
|
|
<div id="header">
|
|
<a
|
|
id="logoname"
|
|
href="./"
|
|
style="text-decoration: none; color: white; margin: 2px"
|
|
>
|
|
<span data-translate="logo-header">
|
|
<font id="qos">O</font>BS.Ninja
|
|
</span>
|
|
</a>
|
|
</div>
|
|
<div id="container">
|
|
<h1>
|
|
OBS.Ninja Speed Test - prototype version
|
|
<small>(Tests connection to TURN server and back)</small>
|
|
</h1>
|
|
</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 load on the right-hand side of the page here. (or press
|
|
CTRL + LeftClick on the new video to open stats that way)
|
|
</li>
|
|
<li>
|
|
Bitrate, Buffer delay, and packet loss are important connection
|
|
quality metrics
|
|
</li>
|
|
<li>
|
|
Change the video bitrate by pressing the buttons below the video. It
|
|
should approach 6000-kbps if the network allows.
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
<div id="statsdiv"></div>
|
|
|
|
<script>
|
|
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>
|