Merge pull request #552 from jcalado/master

Speedtest graphs, data logging, UI revamp
This commit is contained in:
Steve Seguin 2020-12-01 13:06:01 -05:00 committed by GitHub
commit d7c0e0cfcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 465 additions and 185 deletions

View File

@ -92,6 +92,16 @@ button.grey {
color: white; color: white;
} }
button.red {
-webkit-app-region: no-drag;
padding: 10px;
margin: 10px 0px;
cursor: pointer;
border-radius: 2px;
background-color: var(--red-accent);
color: white;
}
button { button {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
padding: 7px 10px 6px 10px; padding: 7px 10px 6px 10px;

119
speedtest.css Normal file
View File

@ -0,0 +1,119 @@
body {
background-color: #141926;
}
h1 {
color: white;
margin: 20px 0px;
}
h1 small {
display: block;
margin-top: 0.5em;
font-size: 0.5em;
}
#container, #graphs, #log {
width: 80%;
margin: 0 auto;
}
#explanation {
color: white;
font-family: sans-serif;
width: 80%;
margin: 0 auto;
margin-top: 20px;
}
#explanation h2 {
border-bottom: 1px solid #383838;
margin-bottom: 10px;
padding-bottom: 5px;
}
iframe {
min-height: 30vh;
width: 39vw;
}
#controls {
margin-top: 20px;
}
#controls button {
margin: 5px;
}
canvas {
background-color: black;
margin: 20px;
}
#log {
margin-top: 20px;
background: #2a2a2a;
padding: 20px 0px;
border: 1px solid #383838;
}
#log ul {
margin: 20px;
list-style: none;
color: #cacaca;
max-height: 20vh;
overflow: auto;
overflow-x: hidden;
}
#graphs {
display: flex;
margin-top: 20px;
background: #2a2a2a;
padding: 20px 0px;
border: 1px solid #383838;
}
.graph {
flex: 1;
position: relative;
}
.graph h2, #log h2 {
margin: 0px 20px;
font-size: 1em;
}
.graph > span {
position: absolute;
bottom: 30px;
left: 30px;
color: #cacaca;
font-weight: bold;
}
ol {
margin-left: 20px;
margin-top: 30px;
}
@media only screen
and (min-device-width: 375px)
and (max-device-width: 812px)
and (orientation: portrait) {
#container {
width: 90%;
}
#graphs {
flex-direction: column;
}
iframe {
width: 90vw;
min-height: 0;
}
}
#statsdiv {display: none;}

View File

@ -1,192 +1,343 @@
<html> <html>
<head><title>OBSN Speed Test</title> <head>
<style> <link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
body{ <link rel="stylesheet" href="./main.css?ver=11" />
padding:0; <link rel="stylesheet" href="./speedtest.css?ver=1" />
margin:0; <meta charset="utf8" />
} <meta name="viewport" content="width=device-width, initial-scale=1" />
iframe { <title>OBSN Speed Test</title>
border:0; <script>
margin:0; (function (w) {
padding:0; w.URLSearchParams =
display:inline-block; w.URLSearchParams ||
margin:0; function (searchString) {
width:100%; var self = this;
height:100%; self.searchString = searchString;
} self.get = function (name) {
span { var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(
border:0; self.searchString
margin:0; );
padding:0; if (results == null) {
display:inline-block; return null;
margin:0; } else {
width:50%; return decodeURI(results[1]) || 0;
height:45%; }
} };
#viewlink { };
width:400px; })(window);
} var urlParams = new URLSearchParams(window.location.search);
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
</style>
<script>
(function (w) { function loadIframe() {
w.URLSearchParams = w.URLSearchParams || function (searchString) { // 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 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 streamID = "";
var urlParams = new URLSearchParams(window.location.search); var possible =
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
for (var i = 0; i < 7; i++) {
streamID += possible.charAt(
Math.floor(Math.random() * possible.length)
);
}
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 iframe = document.createElement("iframe");
var iframeContainer = document.createElement("span");
var streamID = ""; iframe.allow = "autoplay";
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789"; var srcString =
for (var i = 0; i < 7; i++){ "./?push=" +
streamID += possible.charAt(Math.floor(Math.random() * possible.length)); streamID +
} "&cleanoutput&privacy&webcam&audiodevice=0&fullscreen";
if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
}
var iframe = document.createElement("iframe"); // we are changing some text on page load, just to demonstrate what's possible.
var iframeContainer = document.createElement("span"); iframe.onload = function (e) {
e.target.contentWindow.postMessage(
{
function: "changeHTML",
target: "add_camera",
value: "Select your Camera",
},
"*"
);
};
iframe.src = srcString;
iframe.allow="autoplay"; iframeContainer.appendChild(iframe);
var srcString = "./?push="+streamID+"&cleanoutput&privacy&webcam&audiodevice=0&fullscreen"; document.getElementById("container").appendChild(iframeContainer);
if (urlParams.has('turn')){ var iframe = document.createElement("iframe");
srcString = srcString+"&turn="+urlParams.get("turn"); var iframeContainer = document.createElement("span");
}
// we are changing some text on page load, just to demonstrate what's possible. iframe.allow = "autoplay";
iframe.onload = function(e){e.target.contentWindow.postMessage({"function":"changeHTML", "target":"add_camera","value":"Select your Camera"}, '*');}; var srcString = "./?view=" + streamID + "&cleanoutput&privacy&noaudio";
iframe.src = srcString;
iframeContainer.appendChild(iframe); if (urlParams.has("turn")) {
document.getElementById("container").appendChild(iframeContainer); srcString = srcString + "&turn=" + urlParams.get("turn");
}
if (urlParams.has("buffer")) {
srcString = srcString + "&buffer=" + urlParams.get("buffer");
}
iframe.src = srcString;
iframeContainer.appendChild(iframe);
document.getElementById("container").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 = "Disconnect";
button.className = "red";
button.onclick = function () {
iframe.contentWindow.postMessage({ close: true }, "*");
};
buttonContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Low Bitrate";
button.className = "grey";
button.onclick = function () {
iframe.contentWindow.postMessage({ bitrate: 30 }, "*");
};
buttonContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "High Bitrate";
button.className = "grey";
button.onclick = function () {
iframe.contentWindow.postMessage({ bitrate: 6000 }, "*");
};
buttonContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Default Bitrate";
button.className = "grey";
button.onclick = function () {
iframe.contentWindow.postMessage({ bitrate: -1 }, "*");
};
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("Action",e.data.action);
}
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) {
var iframe = document.createElement("iframe"); for (var key in e.data.stats.inbound_stats[streamID]) {
var iframeContainer = document.createElement("span");
iframe.allow="autoplay"; if (key.startsWith("RTCMediaStreamTrack_receiver")) {
var srcString = "./?view="+streamID+"&cleanoutput&privacy&noaudio"; var bitrate =
e.data.stats.inbound_stats[streamID][key][
"Bitrate_in_kbps"
];
plotData("bitrate-graph", bitrate, 6000);
if (urlParams.has('turn')){ var buffer =
srcString = srcString+"&turn="+urlParams.get("turn"); e.data.stats.inbound_stats[streamID][key][
} "Buffer_Delay_in_ms"
];
plotData("buffer-graph", buffer, 200);
if (urlParams.has('buffer')){ var packetloss =
srcString = srcString+"&buffer="+urlParams.get("buffer"); e.data.stats.inbound_stats[streamID][key][
} "packetLoss_in_percentage"
].toFixed(2);
plotData("packetloss-graph", packetloss, 3);
iframe.src = srcString; var resolution =
e.data.stats.inbound_stats[streamID][key][
"Resolution"
]
iframeContainer.appendChild(iframe); if(previousResolution != resolution) {
document.getElementById("container").appendChild(iframeContainer); previousResolution = resolution;
logData("Resolution", resolution);
}
var button = document.createElement("br"); }
document.getElementById("container").appendChild(button); }
var button = document.createElement("button"); document.getElementById("statsdiv").innerHTML =
button.innerHTML = "Disconnect"; "<b>Bitrate (Kbps)</b>" + out.split("Bitrate_in_kbps")[1];
button.onclick = function(){iframe.contentWindow.postMessage({"close":true}, '*');} }
document.getElementById("container").appendChild(button); }
});
var button = document.createElement("button");
button.innerHTML = "Low Bitrate";
button.onclick = function(){iframe.contentWindow.postMessage({"bitrate":30}, '*');}
document.getElementById("container").appendChild(button);
var button = document.createElement("button");
button.innerHTML = "High Bitrate";
button.onclick = function(){iframe.contentWindow.postMessage({"bitrate":6000}, '*');}
document.getElementById("container").appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Default Bitrate";
button.onclick = function(){iframe.contentWindow.postMessage({"bitrate":-1}, '*');}
document.getElementById("container").appendChild(button);
setInterval(function(){iframe.contentWindow.postMessage({"getStats":true}, '*');},1000);
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
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){
document.getElementById("statsdiv").innerHTML = "<b>Bitrate_in_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');
function printValues( obj) { entry.innerText = "[" + new Date().toLocaleTimeString() + "] " + type + " : " + data;
var out = ""; log.prepend(entry);
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 />";
} }
} </script>
} </head>
return out; <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>
</script> <div class="graph">
</head> <h2>Buffer delay (ms)</h2>
<body onload="loadIframe();"> <span>0</span>
<div id="container"> <canvas id="buffer-graph"></canvas>
</div> </div>
<div style="width:48%;padding:0;margin:0;border:0;display:inline-block;">
<h3>OBS.Ninja Speed Test - prototype version</h3> <div class="graph">
(Tests connection to TURN server and back)<br /><br /> <h2>Packet Loss (%)</h2>
<li>1.Select your camera.</li> <span>0</span>
<li>2.Hit start</li> <canvas
<li>3.Wait for the video to load side-by-side. *If it does not auto-load within 20s, refresh and try again.*</li> id="packetloss-graph"
<li>4.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> ></canvas>
<li>5.Bitrate_in_kbps, Buffer_Delay_in_ms, and packetLoss_percentage are important connection quality metrics</li> </div>
<li>6.Increase the video bitrate by pressing <i>High Bitrate</i>; it should approach 6000-kbps if the network allows.</li> </div>
</div> <div id="log">
<div id="statsdiv" style="width:48%;padding:0;margin:0;border:0;display:inline-block;"> <h2>Log</h2>
</div> <ul></ul>
</body> </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>
function plotData(element, data, max) {
var canvas;
var context;
var yScale;
canvas = document.getElementById(element);
context = canvas.getContext("2d");
if (isNaN(data)) {
data = 0;
}
var text = (canvas.previousElementSibling.innerHTML = data);
var height = context.canvas.height;
var width = context.canvas.width;
context.fillStyle = "#009933";
context.imageSmoothingEnabled = true;
yScale = height / max;
if (data > max) {
data = max;
}
context.fillRect(
width - 1,
height - data * yScale,
1,
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> </html>