v24b, with cloudflare/whip->whep auto viewer support

This commit is contained in:
steveseguin 2023-08-28 04:36:45 -04:00
parent 676d831e20
commit 7154b90c03
13 changed files with 1385 additions and 553 deletions

View File

@ -176,7 +176,7 @@
<script>
function getChromeVersion() {
function getChromiumVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
@ -339,6 +339,10 @@
logData({"timestart": Date.now()});
try {
logData({"peakhour": new Date(Date.now()).getHours()>7 && new Date(Date.now()).getHours()<11});
} catch(e){}
var showdetails = document.createElement("button");
showdetails.onclick = function(){
document.getElementById("graphs").classList.toggle('hidden');
@ -377,7 +381,7 @@
for (var someValue in e.data.stats.inbound_stats) {
out += printValues(e.data.stats.inbound_stats[someValue]);
if (e.data.stats.inbound_stats[someValue]){
if (e.data.stats.inbound_stats[someValue] && e.data.stats.inbound_stats[someValue].info){
if (!statsSent){
statsSent = e.data.stats.inbound_stats[someValue];
}
@ -502,7 +506,7 @@
iframe1.allowfullscreen ="true";
//iframe.allow = "autoplay";
var srcString = "./?push=" + streamID + "&cleanoutput&privacy&"+testType+"&audiodevice=1&fullscreen&transparent&remote&maxbandwidth&speedtest="+zone;
var srcString = "./index.html?push=" + streamID + "&cleanoutput&privacy&"+testType+"&audiodevice=1&fullscreen&transparent&remote&maxbandwidth&speedtest="+zone;
if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
@ -555,7 +559,7 @@
var iframeContainer = document.createElement("span");
iframe.allow = "autoplay";
var srcString = "./?view=" + streamID + "&cleanoutput&privacy&noaudio&transparent&bitrate=6000&scale=100&speedtest="+zone; // No TURN servers set on the reciever. Don't want to query for TURN servers needlessly.
var srcString = "./index.html?view=" + streamID + "&cleanoutput&privacy&noaudio&transparent&bitrate=6000&scale=100&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");
@ -673,7 +677,7 @@
var testType= "webcam&quality=0&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>';
document.getElementById("screen").innerHTML = '<a href="./speedtest.html" style="color: #CCC;">Test webcam-streaming performance here</a>';
testType = "quality=0&screenshare&css=speedtest.css"
}

267
cloudflare.html Normal file
View File

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html>
<head>
<title>Generate Cloudflare Auth</title>
<style>
body {
font-family: Arial, sans-serif;
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
background-color: #f0f0f0;
background-image:
linear-gradient(to right, #e0e0e0 1px, transparent 1px),
linear-gradient(to bottom, #e0e0e0 1px, transparent 1px);
background-size: 10px 10px;
backdrop-filter: blur(3px);
}
h1 {
color: #333;
margin-bottom: 20px;
}
form {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
width: 300px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
}
input[type="text"],
input[type="password"],
input[type="number"] {
width: 93%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 4px;
}
input[type="number"] {
max-width:80px;
}
button {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
textarea {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
max-width: 500px;
min-height: 100px;
}
.section{
max-width:700px;
padding: 20px;
overflow: auto;
}
.main {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
align-content: space-around;
justify-content: center;
}
.secondary {
padding: 50px;
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
align-content: space-around;
justify-content: center;
max-width:1200px;
margin:auto;
}
</style>
</head>
<body>
<div class='main'>
<h1>Generate Cloudflare Auth for VDO.Ninja</h1>
<form id="postForm">
<label for="userId">Cloudflare Account ID:</label>
<input type="text" id="userId" name="userId" required>
<br><br>
<label for="accessToken">Cloudflare Stream Access Token:</label>
<input type="password" id="accessToken" name="accessToken" required>
<br><br>
<label for="expiration">Hours until expiration</label>
<input min="0" type="number" id="expiration" name="expiration" placeholder="optional">
<br><br>
<button type="button" id="submitButton">Generate</button>
</form>
<br><br>
<label for="response">Generated URL parameter to add to VDO.Ninja:</label>
<textarea id="response" rows="5" cols="50" readonly placeholder="Use this parameter with your VDO.Ninja links in place of &meshcast"></textarea>
<div class="section">
<h2>
What you can do with Cloudflare + VDO.Ninja?
</h2>
<h3>Restream VDO.Ninja as an RTMP Output</h3>
<p>Live Video Inputs (Cloudflare feature) can be set up to forward any input to another input. This can be a RTMP(S) service such as YouTube, Twitch or Facebook Live.</p>
<p>In theory you could publish from VDO.Ninja WHIP output to Cloudstream, and then to your RTMP destinations, like Youtube.</p>
<p></p>
<h3>Meshcast-alternative</h3>
<p>Instead of using Meshcast to broadcast video from director to guest, or guest to scene, you can use Cloudflare instead.</p>
<p>Meshcast, or any compatible WHIP/WHEP service, can help reduce CPU and network load of guests by offloading distribution to a server, compared to using the peer-to-peer default of VDO.Ninja
<h3>Automatic isolated guest recording</h3>
<p>Cloudflare will automatically record incoming videos, allowing you (in theory) to have a backup of each guest in a room.</p>
<p>This offers a redundant backup for your recordings, but also makes it easier to do higher quality VODs edits after the live ends.</p>
<h3>SRT, HLS, DASH, MP4, WHIP/WHEP options</h3>
<p>Lots input and output options, although if you're here, you're probably interested in the WHIP/WHEP mainly.</p>
<p>VDO.Ninja is compatible with WHEP and WHIP!</p>
<h3>Very competitive pricing</h3>
<p>There's a free tier, which is more than enough for testing.</p>
<p>Or pay $1 per $1000 minutes of streaming.</p>
</div>
</div>
<div class='secondary'>
<h2>
How it works?
</h2>
<p>When used with VDO.Ninja, video is published to Cloudflare via WHIP, and the WHEP playback URL is distributed to viewers. Unless otherwise specified, viewers will use the WHEP URL as the source of media from the publisher, instead of using the normal peer-to-peer mode. This has the effect of reducing the CPU and network load when sharing media with multiple videos, as instead of distributing media via peer-to-peer, the media is distributed via a server. This approach does have some downsides also, so its not normally advisable unless desired or needed.</p>
<h2>
Why do I need a special URL parameter?
</h2>
<p>The reason we need a special generated URL parameter is because Cloudflare requires user accounts, unlike Meshcast. While you can generate WHIP URLs within your Cloudflare dashboard, and use them on VDO.Ninja links using &whipout, you'd need to create one per guest. Instead here, we're using our Cloudflare credentials to automatically create unique WHIP ingest URLs on demand for each guest, so you can get away with one-invite link for all your guests.</p>
<p>Since it's not advisable to share your Cloudflare credentials, particularly with random guests, this page will encrypt your credentials into URL-friendly parameter. Only the VDO.Ninja servers knows the decryption key, which limits what guest can do with the encrypted key. You can delete or restrict the credentials provided to VDO.Ninja from your Cloudflare dashboard, allowing you to limit or revoke any trust provided to VDO.Ninja.</p>
<h2>
Where to get my Cloudflare account ID and token?
</h2>
<p>The Cloudflare account ID can be found on the right-hand side of the Workers & Pages (Overview) page, or it can be found on the right-lower side of any of your Website (domain) overview pages.</p>
<p>
As for the API token, you'll need to create it, with limited permissions.
<ul>
<li>Go to <a href='https://dash.cloudflare.com/profile/api-tokens' target="_blank">https://dash.cloudflare.com/profile/api-tokens</a></li>
<li>Click to Create Token (API Tokens)</li>
<li>Click Get started with the Create Custom Token option</li>
<li>Provide a token name, and for the permissions select Account -> Stream -> Edit</li>
<li>You can define a time-to-live (TTL), if you wish for the token to auto-expire.</li>
</ul>
You should now have access to both access token and account ID.
</p>
<h2>
Can I self-host or hard-code my Cloudflare credentials?
</h2>
<p>Yes, the code is open-sourced and it can be self-hosted, however please be aware there is limited support for those self-hosting.</p>
<h2>
Does VDO.Ninja track me or store my private information?
</h2>
<p>Please refer to the <a href='https://docs.vdo.ninja/help/privacy-and-security-details'>privacy policy</a>, although the short answer is no. I can't say the same for Cloudflare, so please refer to their terms of service.
</div>
<script>
function removeStorage(cname){
localStorage.removeItem(cname);
}
function clearStorage(){
localStorage.clear();
if (!session.cleanOutput){
warnUser("The local storage and saved settings have been cleared", 1000);
}
}
function setStorage(cname, cvalue, hours=9999){ // not actually a cookie
var now = new Date();
var item = {
value: cvalue,
expiry: now.getTime() + (hours * 60 * 60 * 1000),
};
try{
localStorage.setItem(cname, JSON.stringify(item));
}catch(e){errorlog(e);}
}
function getStorage(cname) {
try {
var itemStr = localStorage.getItem(cname);
} catch(e){
errorlog(e);
return;
}
if (!itemStr) {
return "";
}
var item = JSON.parse(itemStr);
var now = new Date();
if (now.getTime() > item.expiry) {
localStorage.removeItem(cname);
return "";
}
return item.value;
}
document.getElementById("accessToken").value = getStorage("accessToken") || "";
document.getElementById("userId").value = getStorage("userId") || "";
document.getElementById("expiration").value = getStorage("expiration") || "";
document.getElementById("submitButton").addEventListener("click", async function () {
const accessToken = document.getElementById("accessToken").value;
const userId = document.getElementById("userId").value;
const expiration = document.getElementById("expiration").value;
if (!accessToken || !userId) {
alert("Access Token and User ID are required.");
return;
}
const data = {
accessToken: accessToken,
userId: userId,
expiration: Math.round(expiration*60)
};
try {
const response = await fetch("https://cloudflare.vdo.ninja/encode/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
const responseData = await response.text();
console.log(responseData);
setStorage("accessToken",accessToken);
setStorage("userId", userId);
setStorage("expiration", expiration);
document.getElementById("response").value = "&cftoken="+responseData;
} catch (error) {
console.error("Error:", error);
document.getElementById("response").value = "An error occurred.";
}
});
</script>
</body>
</html>

View File

@ -24,9 +24,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta content="utf-8" http-equiv="encoding" />
<meta name="copyright" content="&copy; 2022 Steve Seguin" />
<meta name="copyright" content="&copy; 2023 Steve Seguin" />
<meta name="license" content="https://github.com/steveseguin/vdo.ninja/LICENSE.md" />
<meta name="sourcecode" content="https://github.com/steveseguin/vdo.ninja" />
<meta name="stance-on-war" content="Steve Seguin condemns Russia's brutal invasion of Ukraine 💙💛." />
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<link id="favicon1" rel="icon" type="image/png" sizes="32x32" href="./media/favicon-32x32.png" />
<link id="favicon2" rel="icon" type="image/png" sizes="16x16" href="./media/favicon-16x16.png" />
@ -56,10 +58,15 @@
<meta property="twitter:image" content="./media/vdoNinja_logo_full.png" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
<link rel="stylesheet" href="./main.css?ver=354" />
<link rel="stylesheet" href="./main.css?ver=361" />
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script>
<style id="lightbox-animations" type="text/css"></style>
<!-- Until Chrome v115 ends ; pip2 -->
<meta http-equiv="origin-trial" content="AoalTVyEOoiQninAV09SzviYaAtRKuTfDIsMUNQLIg1/+ZWOpXEFOL+GQGqkQzkkszPrK26oGzB1hIF3beHJjAMAAABeeyJvcmlnaW4iOiJodHRwczovL3Zkby5uaW5qYTo0NDMiLCJmZWF0dXJlIjoiRG9jdW1lbnRQaWN0dXJlSW5QaWN0dXJlQVBJIiwiZXhwaXJ5IjoxNjk0MTMxMTk5fQ==">
<!-- Until Chrome v117 ends ; blur -->
<meta http-equiv="origin-trial" content="Aqwjtr1IS9AdkcWCFAOHtMMmsKDy8Ti58hQBbHkR/HnloiMhkW17cYgnkiLgOH9zuTDC/o4GquQ0MHe9tqT51wcAAABdeyJvcmlnaW4iOiJodHRwczovL3Zkby5uaW5qYTo0NDMiLCJmZWF0dXJlIjoiTWVkaWFDYXB0dXJlQmFja2dyb3VuZEJsdXIiLCJleHBpcnkiOjE2OTg5Njk1OTl9">
<!-- <link rel="manifest" href="manifest.json" /> -->
<!-- ios support
@ -83,9 +90,9 @@
<link itemprop="url" href="./media/vdoNinja_logo_full.png" />
</span>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=48"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=49"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/aes.js"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=682"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=686"></script>
<input id="zoomSlider" type="range" style="display: none;" />
<span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;-webkit-app-region: drag;min-height:20px;"></span>
<div id="header">
@ -2193,41 +2200,21 @@
<span style="margin: 5px 0 0 0;display:block">
<button data-action-type="local-global-record" title="Record all the guests" onclick="
document.querySelectorAll('[data-action-type=\'recorder-local\']').forEach(target=>{
recordVideo(target);
});">
<button data-action-type="local-global-record" title="Record all the guests" onclick="localGlobalRecordStart();">
<i class="las la-compact-disc"></i>
<span data-translate="local-global-record">Local record - start all</span>
<span data-translate="local-global-record-start">Local record - start all</span>
</button>
<button data-action-type="local-global-record" title="Record all the guests" onclick="
document.querySelectorAll('[data-action-type=\'recorder-local\']').forEach(target=>{
recordVideo(target);
});">
<button data-action-type="local-global-record" title="Record all the guests" onclick="localGlobalRecordStop();">
<i class="las la-square"></i>
<span data-translate="local-global-record">Local record - stop all</span>
<span data-translate="local-global-record-stop">Local record - stop all</span>
</button>
</span>
<span style="margin: 5px 0 0 0;display:block">
<button data-action-type="remote-global-record" title="Record all the guests" onclick="
window.focus();
async function jjj(){
var bitrate = await promptAlt(miscTranslations['what-bitrate'], false, false, 6000);
document.querySelectorAll('[data-action-type=\'recorder-remote\']').forEach(target=>{
requestVideoRecord(target, true, bitrate);
});
}
jjj();
">
<button data-action-type="remote-global-record" title="Record all the guests" onclick="remoteGlobalRecordStart();">
<i class="las la-compact-disc"></i>
<span data-translate="remote-global-record">Remote record - start all</span>
</button>
<button data-action-type="remote-global-record" title="Record all the guests" onclick="
document.querySelectorAll('[data-action-type=\'recorder-remote\']').forEach(target=>{
if (target.classList.contains('pressed')){
requestVideoRecord(target, false);
}
});">
<button data-action-type="remote-global-record" title="Record all the guests" onclick="remoteGlobalRecordStop();">
<i class="las la-square"></i>
<span data-translate="remote-global-record">Remote record - stop all</span>
</button>
@ -2509,10 +2496,7 @@
</u>
</div>
<span class="hidden" id="hangupTemplate">
<span style='font-size:500%;text-align:center;margin:auto;'>👋<br><button onClick='parent.location.reload();' title="Reload the page" data-translate="reload-page">Refresh</button></span>
</span>
<span class="hidden" id="hangupTemplateMobileFullscreen">
<span style='font-size:500%;text-align:center;margin:auto;'>👋<br><button onClick='parent.location.reload();' title="Reload the page" data-translate="reload-page">Refresh</button></span>
<span id="hangupContainer">👋<br><button onClick='parent.location.reload();' title="Reload the page" data-translate="reload-page">Refresh</button></span>
</span>
<div id="meshcastMenu" class="hidden">
Meshcast publishing region: <select name="edgelist" id="edgelist" onchange="selectMeshcast(this);" title="Select a location that is closest to both you and your audience."></select>
@ -2529,7 +2513,7 @@
var session = WebRTC.Media; // session is a required global variable if configuring manually. Run before loading main.js but after webrtc.js.
session.version = "23.9";
session.version = "24.0b";
session.streamID = session.generateStreamID(); // randomly generates a streamID for this session. You can set your own programmatically if needed
session.defaultPassword = "someEncryptionKey123"; // Change this password if self-deploying for added security/privacy
@ -2642,11 +2626,11 @@
// session.hidehome = true; // If used, 'hide home' will make the landing page inaccessible, along with hiding a few go-home elements.
// session.record = false; // uncomment to block users from being able to record via vdo.ninja's built in recording function
</script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=886"></script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=897"></script>
<!--
// If you wish to change branding, blank offers a good clean start.
<script type="text/javascript" id="main-js" src="./main.js" data-translation="blank"></script>
-->
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=702"></script>
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=715"></script>
</body>
</html>

1210
lib.js

File diff suppressed because it is too large Load Diff

View File

@ -805,10 +805,9 @@ hr {
border-radius: var(--video-rounded);
}
#gridlayout,#directorlayout {
#gridlayout, #directorlayout {
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
justify-items: stretch;
border: 0;
@ -1606,12 +1605,21 @@ body.darktheme{
display: inline-block;
width:100%;
}
#welcomeImage{
object-fit:cover;
width:100%;
height:100%;
display:block;
z-index:10;
.welcomeOverlay{
object-fit: cover;
width: 100%;
height: 100%;
display: block;
position: absolute;
left: 0;
z-index: 500;
top: 0;
animation: fadeIn 0.1s;
-webkit-animation: fadeIn 0.3s;
-moz-animation: fadeIn 0.3s;
-o-animation: fadeIn 0.3s;
-ms-animation: fadeIn 0.3s;
animation-iteration-count: 1;
}
div[data-action-type='toggle-group'] {
padding: 0 10px;
@ -3708,7 +3716,20 @@ div#roomnotes2 {
background-color: var(--discord-grey-3);
color: var(--discord-text);
}
#hangupContainer {
font-size: 500%;
text-align: center;
margin: auto auto;
display: flex;
height: 100%;
width: 100%;
vertical-align: middle;
flex-wrap: wrap;
align-content: center;
flex-direction: column;
justify-content: center;
align-items: stretch;
}
.controlCenterBox .flexBreak span {
position: absolute;
top: 50%;

237
main.js
View File

@ -372,6 +372,13 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
}
if (urlParams.has('cftoken') || urlParams.has('cft')){
session.whipOutput = urlParams.get('cftoken') || urlParams.get('cft') || false;
if (session.whipOutput){
session.whipOutput = "https://cloudflare.vdo.ninja/"+session.whipOutput;
}
}
if (urlParams.has('whippush') || urlParams.has('whipout') || urlParams.has('pushwhip')) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case
session.whipOutput = urlParams.get('whippush') || urlParams.get('whipout') || urlParams.get('pushwhip') || null;
if (session.whipOutput){
@ -389,18 +396,20 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
getById("publishOutURL").classList.remove("hidden");
}
}
if (urlParams.has('whippushtoken') || urlParams.has('whipouttoken') || urlParams.has('pushwhiptoken')) {// URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case
session.whipOutputToken = urlParams.get('whippushtoken') || urlParams.get('whipouttoken') || urlParams.get('pushwhiptoken') || false;
if (!session.whipOutputToken){
getById("publishOutToken").classList.remove("hidden");
}
} else if (session.whipOutput!==false){
if (!session.whipOutputToken){
getById("publishOutToken").classList.remove("hidden");
if (urlParams.has('whippushtoken') || urlParams.has('whipouttoken') || urlParams.has('pushwhiptoken')) {// URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case
session.whipOutputToken = urlParams.get('whippushtoken') || urlParams.get('whipouttoken') || urlParams.get('pushwhiptoken') || false;
if (!session.whipOutputToken){
getById("publishOutToken").classList.remove("hidden");
}
} else if (session.whipOutput!==false){
if (!session.whipOutputToken){
getById("publishOutToken").classList.remove("hidden");
}
}
}
if (urlParams.has('whepplay')) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case
if (urlParams.get('whepplay')){
try {
@ -618,8 +627,6 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.meshcastCode = urlParams.get('meshcastcode') || urlParams.get('mccode') || false
}
if (urlParams.has('nomeshcast')) {
session.noMeshcast = urlParams.get('nomeshcast') || true;
}
@ -759,12 +766,6 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
}
if (urlParams.has("statsinterval")){
session.statsInterval = parseInt(urlParams.get("statsinterval")) || 3000; // milliseconds. interval of requesting stats of remote guests
}
if (urlParams.has('rotate') ) {
session.rotate = urlParams.get('rotate') || 90;
session.rotate = parseInt(session.rotate);
@ -860,9 +861,26 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.hangupbutton = false;
}
if (urlParams.has('hangupbutton') || urlParams.has('hub')){
if (urlParams.has('hangupbutton') || urlParams.has('hub') || urlParams.has('humb64')){
session.hangupbutton = true;
}
if (urlParams.has('hangupmessage') || urlParams.has('hum') || urlParams.has('humb64')){
let htmlmessage = urlParams.get("hangupmessage") || urlParams.get("hum") || urlParams.get('humb64');
if (urlParams.get('humb64')){
try {
htmlmessage = atob(htmlmessage);
} catch(e){}
}
try {
htmlmessage = htmlmessage.replace(/(\r\n|\n|\r)/gm, '');
htmlmessage = decodeURIComponent(htmlmessage);
} catch(e){console.error(e);}
getById("hangupContainer").innerHTML = htmlmessage;
}
if (urlParams.has('socialstream')){
session.socialstream = urlParams.get('socialstream') || false;
@ -1503,7 +1521,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
getById("defaultAvatar2").classList.add("selected");
}
} else if (avatar){
avatar = decodeURIComponent(avatar);
try {
avatar = decodeURIComponent(avatar);
}catch(e){}
session.avatar = getById("defaultAvatar2");
session.avatar.ready = false;
@ -1593,7 +1613,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.password = false;
session.defaultPassword = false;
} else {
session.password = decodeURIComponent(session.password); // will be re-encoded in a moment.
try {
session.password = decodeURIComponent(session.password); // will be re-encoded in a moment.
} catch(e){errorlog(e);}
}
} else if (urlParams.has('nopassword') || urlParams.has('nopass') || urlParams.has('nopw') || urlParams.has('p0')) {
session.password = false;
@ -1690,7 +1712,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.label = await promptAlt(getTranslation("enter-display-name"), true);
} else {
var updateURLAsNeed = false;
session.label = decodeURIComponent(session.label);
try {
session.label = decodeURIComponent(session.label);
} catch(e){errorlog(e);}
session.label = session.label.replace(/_/g, " ")
}
if (session.label != null) {
@ -2503,7 +2527,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
try {
session.retryWatchInterval();
} catch(e){
warnlog(e);
log(e);
clearTimeout(this);
}
}, session.forceRetry*1000);
@ -2834,7 +2858,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.mainDirectorPassword = await promptAlt(getTranslation("director-password"), true, true);
if (session.mainDirectorPassword){
session.mainDirectorPassword = session.mainDirectorPassword.trim();
try {
session.mainDirectorPassword = decodeURIComponent(session.mainDirectorPassword);
} catch(e){errorlog(e);}
}
}
// registerToken();
@ -3027,8 +3053,8 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.sharperScreen = true;
}
if (urlParams.has('mcscale') || urlParams.has('meshcastscale')) {
session.meshcastScale = parseFloat(urlParams.get('mcscale')) || parseFloat(urlParams.get('meshcastscale')) || 100;
if (urlParams.has('mcscale') || urlParams.has('meshcastscale') || urlParams.has('woscale') || urlParams.has('whipoutscale')) {
session.whipOutScale = parseFloat(urlParams.get('mcscale')) || parseFloat(urlParams.get('meshcastscale')) || parseFloat(urlParams.get('woscale')) || parseFloat(urlParams.get('whipoutscale')) || 100;
}
@ -3078,14 +3104,38 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.noExitPrompt = true;
}
if (urlParams.has('entrymsg') || urlParams.has('welcome')) {
session.welcomeMessage = urlParams.get('entrymsg') || urlParams.get('welcome');
session.welcomeMessage = decodeURIComponent(session.welcomeMessage);
if (urlParams.has('entrymsg') || urlParams.has('welcome') || urlParams.has('welcomeb64')) {
session.welcomeMessage = urlParams.get('entrymsg') || urlParams.get('welcome') || urlParams.get('welcomeb64');
if (urlParams.get('welcomeb64')){
try {
session.welcomeMessage = atob(session.welcomeMessage);
} catch(e){}
}
try {
session.welcomeMessage = session.welcomeMessage.replace(/(\r\n|\n|\r)/gm, ' ');
session.welcomeMessage = decodeURIComponent(session.welcomeMessage);
} catch(e){}
}
if (urlParams.has('welcomehtml')) {
session.welcomeHTML = urlParams.get('welcomehtml');
try {
session.welcomeHTML = atob(session.welcomeHTML);
} catch(e){}
try {
session.welcomeHTML = session.welcomeHTML.replace(/(\r\n|\n|\r)/gm, ' ');
session.welcomeHTML = decodeURIComponent(session.welcomeHTML);
} catch(e){}
}
if (urlParams.has('welcomeimage') || urlParams.has('welcomeimg')) {
session.welcomeImage = urlParams.get('welcomeimage') || urlParams.get('welcomeimg');
session.welcomeImage = decodeURIComponent(session.welcomeImage);
try {
session.welcomeImage = decodeURIComponent(session.welcomeImage);
} catch(e){}
}
if (urlParams.has('mixminus')){
@ -3165,62 +3215,46 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.limitTotalBitrate = parseInt(session.limitTotalBitrate);
}
if (urlParams.has('mcb') || urlParams.has('mcbitrate') || urlParams.has('meshcastbitrate')){
session.meshcastBitrate = urlParams.get('mcb') || urlParams.get('mcbitrate') || urlParams.get('meshcastbitrate') || 2500;
session.meshcastBitrate = parseInt(session.meshcastBitrate);
if (urlParams.has('mcscreensharebitrate') || urlParams.has('mcssbitrate') || urlParams.has('whipoutscreensharebitrate') || urlParams.has('wossbitrate')){
session.whipOutScreenShareBitrate = urlParams.get('mcscreensharebitrate') || urlParams.get('mcssbitrate') || urlParams.get('whipoutscreensharebitrate') || urlParams.get('wossbitrate') || 2500;
session.whipOutScreenShareBitrate = parseInt(session.whipOutScreenShareBitrate);
}
if (urlParams.has('mcscreensharebitrate') || urlParams.has('mcssbitrate')){
session.meshcastScreenShareBitrate = urlParams.get('mcscreensharebitrate') || urlParams.get('mcssbitrate') || 2500;
session.meshcastScreenShareBitrate = parseInt(session.meshcastScreenShareBitrate);
if (urlParams.has('mcscreensharecodec') || urlParams.has('mcsscodec') || urlParams.has('whipoutscreensharecodec') || urlParams.has('wosscodec')){
session.whipOutScreenShareCodec = urlParams.get('mcscreensharecodec') || urlParams.get('mcsscodec') || urlParams.get('whipoutscreensharecodec') || urlParams.get('wosscodec') || false;
}
if (session.whipOutScreenShareCodec){
session.whipOutScreenShareCodec = session.whipOutScreenShareCodec.toLowerCase();
}
if (urlParams.has('mcscreensharecodec') || urlParams.has('mcsscodec')){
session.meshcastScreenShareCodec = urlParams.get('mcscreensharecodec') || urlParams.get('mcsscodec') || false;
}
if (session.meshcastScreenShareCodec){
session.meshcastScreenShareCodec = session.meshcastScreenShareCodec.toLowerCase();
}
if (urlParams.has('mcab') || urlParams.has('mcaudiobitrate') || urlParams.has('meshcastab') || urlParams.has('meshcastaudiobitrate ')){
session.meshcastAudioBitrate = urlParams.get('mcab') || urlParams.get('mcaudiobitrate') || urlParams.get('meshcastab') || urlParams.get('meshcastaudiobitrate ') || 32;
session.meshcastAudioBitrate = parseInt(session.meshcastAudioBitrate);
}
if (urlParams.has('mccodec') || urlParams.has('meshcastcodec')){
session.meshcastCodec = urlParams.get('mccodec') || urlParams.get('meshcastcodec') || false;
}
if (session.meshcastCodec){
session.meshcastCodec = session.meshcastCodec.toLowerCase();
if (session.meshcastCodec == "h264"){
if (Firefox){
session.meshcastCodec = false;
}
}
}
if (urlParams.has('whipoutcodec') || urlParams.has('woc')){
session.whipOutCodec = urlParams.get('whipoutcodec') || urlParams.get('woc') || false;
if (urlParams.has('mccodec') || urlParams.has('meshcastcodec') || urlParams.has('whipoutcodec') || urlParams.has('woc')){
session.whipOutCodec = urlParams.get('mccodec') || urlParams.get('meshcastcodec') || urlParams.get('whipoutcodec') || urlParams.get('woc') || false;
getById("whipoutcodecGroupFlag").classList.add("hidden");
}
if (session.whipOutCodec){
session.whipOutCodec = session.whipOutCodec.toLowerCase();
if (session.whipOutCodec == "h264"){
if (Firefox){
session.whipOutCodec = false;
}
}
if (session.whipOutCodec){
session.whipOutCodec = session.whipOutCodec.split(',');
}
getById("whipoutcodecGroupFlag").classList.add("hidden");
}
if (urlParams.has('whipoutaudiobitrate') || urlParams.has('woab')){
session.whipOutAudioBitrate = urlParams.get('whipoutaudiobitrate') || urlParams.get('woab') || false;
if (urlParams.has('mcab') || urlParams.has('mcaudiobitrate') || urlParams.has('meshcastab') || urlParams.has('meshcastaudiobitrate ') || urlParams.has('whipoutaudiobitrate') || urlParams.has('woab')){
session.whipOutAudioBitrate = urlParams.get('mcab') || urlParams.get('mcaudiobitrate') || urlParams.get('meshcastab') || urlParams.get('meshcastaudiobitrate ') || urlParams.get('whipoutaudiobitrate') || urlParams.get('woab') || false;
if (session.whipOutAudioBitrate ){
session.whipOutAudioBitrate = parseInt(session.whipOutAudioBitrate );
}
getById("whipoutaudiobitrate").classList.add("hidden");
}
if (urlParams.has('whipoutvideobitrate') || urlParams.has('wovb')){
session.whipOutVideoBitrate = urlParams.get('whipoutvideobitrate') || urlParams.get('wovb') || false;
if (urlParams.has('mcb') || urlParams.has('mcbitrate') || urlParams.has('meshcastbitrate') || urlParams.has('whipoutvideobitrate') || urlParams.has('wovb')){
session.whipOutVideoBitrate = urlParams.get('mcb') || urlParams.get('mcbitrate') || urlParams.get('meshcastbitrate') || urlParams.get('whipoutvideobitrate') || urlParams.get('wovb') || false;
if (session.whipOutVideoBitrate){
session.whipOutVideoBitrate = parseInt(session.whipOutVideoBitrate);
}
@ -3306,9 +3340,30 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
if (urlParams.has('stats')) {
session.statsMenu = true;
if (urlParams.get('stats') == "0") {
session.statsMenu = false;
} else if (urlParams.get('stats') == "false") {
session.statsMenu = false;
} else if (urlParams.get('stats') == "off") {
session.statsMenu = false;
} else {
session.statsMenu = true;
}
} else if (urlParams.has('nostats')) {
session.statsMenu = false;
}
if (session.statsMenu === false){ // hide menu option
try {
document.queryselector('[data-action="ShowStats"]').parentNode.classList.add("hidden");
} catch(e){}
}
if (urlParams.has("statsinterval")){
session.statsInterval = parseInt(urlParams.get("statsinterval")) || 3000; // milliseconds. interval of requesting stats of remote guests
}
if (urlParams.has('cleandirector') || urlParams.has('cdv')) {
session.cleanDirector = true;
}
@ -3523,6 +3578,10 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
}
if (urlParams.has('nomirror')) {
session.nomirror = true;
}
if (urlParams.has('mirror')) {
if (urlParams.get('mirror') == "3") {
getById("main").classList.add("mirror");
@ -3764,6 +3823,8 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.audioEffects = true;
}
// if (session.audioCodec === "lyra"){ // WIP. does not work
// try {
// var { default: Module } = await import('./thirdparty/lyra/webassembly_codec_wrapper.js');
@ -3793,6 +3854,10 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.micSampleRate = parseInt(urlParams.get('micsamplerate')) || parseInt(urlParams.get('msr')) || 48000;
}
if (urlParams.has('micsamplesize')) {
session.micSampleSize = parseInt(urlParams.get('micsamplesize')) || 16;
}
if (urlParams.has('noaudioprocessing') || urlParams.has('noap')) {
session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers?
session.disableViewerWebAudioPipeline = true; // this has the potential to break things.
@ -4271,37 +4336,41 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.cleanOutput = true;
}
}
if (urlParams.has('clock')){
if (urlParams.has('clock') || urlParams.has('clock24')){
let urlClock = urlParams.get('clock') || urlParams.get('clock24');
if (urlParams.has('clock24')){
session.clock24 = true;
}
session.showTime = true;
if (urlParams.get('clock') === "false"){
if (urlClock === "false"){
session.showTime = false;
} else if (urlParams.get('clock') === "0"){
} else if (urlClock === "0"){
session.showTime = false;
} else if (urlParams.get('clock') === "1"){
} else if (urlClock === "1"){
getById("overlayClockContainer2").classList.add("top");
getById("overlayClockContainer2").classList.add("left");
} else if (urlParams.get('clock') === "7"){
} else if (urlClock === "7"){
getById("overlayClockContainer2").classList.add("bottom");
getById("overlayClockContainer2").classList.add("left");
} else if (urlParams.get('clock') === "4"){
} else if (urlClock === "4"){
getById("overlayClockContainer2").classList.add("vmiddle");
getById("overlayClockContainer2").classList.add("left");
} else if (urlParams.get('clock') === "2"){
} else if (urlClock === "2"){
getById("overlayClockContainer2").classList.add("top");
getById("overlayClockContainer2").classList.add("hmiddle");
} else if (urlParams.get('clock') === "8"){
} else if (urlClock === "8"){
getById("overlayClockContainer2").classList.add("bottom");
getById("overlayClockContainer2").classList.add("hmiddle");
} else if (urlParams.get('clock') === "5"){
} else if (urlClock === "5"){
getById("overlayClockContainer2").classList.add("vmiddle");
getById("overlayClockContainer2").classList.add("hmiddle");
} else if (urlParams.get('clock') === "3"){
} else if (urlClock === "3"){
getById("overlayClockContainer2").classList.add("top");
getById("overlayClockContainer2").classList.add("right");
} else if (urlParams.get('clock') === "9"){
} else if (urlClock === "9"){
getById("overlayClockContainer2").classList.add("bottom");
getById("overlayClockContainer2").classList.add("right");
} else if (urlParams.get('clock') === "6"){
} else if (urlClock === "6"){
getById("overlayClockContainer2").classList.add("vmiddle");
getById("overlayClockContainer2").classList.add("right");
}
@ -4399,7 +4468,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
if (urlParams.has('screensharelabel') || urlParams.has('sslabel')) {
session.screenShareLabel = urlParams.get('screensharelabel') || urlParams.get('sslabel');
try {
session.screenShareLabel = decodeURIComponent(session.screenShareLabel);
} catch(e){}
session.screenShareLabel = session.screenShareLabel.replace(/_/g, " ")
}
@ -4559,7 +4630,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
window.focus();
session.directorPassword = await promptAlt(getTranslation("enter-director-password"), true);
} else {
session.directorPassword = decodeURIComponent(session.directorPassword);
try {
session.directorPassword = decodeURIComponent(session.directorPassword);
} catch(e){}
}
if (session.directorPassword){
session.directorPassword = sanitizePassword(session.directorPassword)
@ -5080,7 +5153,11 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
} else if (e.data.record){
var video = document.getElementById(e.data.record);
if (video){
recordLocalVideo(null, 4000, video);
var videoKbps = 4000;
if (session.recordLocal !== false) {
videoKbps = session.recordLocal;
}
recordLocalVideo(null, videoKbps, video);
}
}
}

View File

@ -209,11 +209,11 @@
</style>
<script>
function getChromeVersion() {
function getChromiumVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
if (!getChromeVersion()){
if (!getChromiumVersion()){
alert("This speedtest is optimized for Chromium-based browsers; graphs will not work for Firefox or Safari browsers.");
}

View File

@ -94,7 +94,7 @@
<script>
function getChromeVersion() {
function getChromiumVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
@ -233,6 +233,7 @@
var PAKCCC = 0;
function process(arr) {
var maxResolution = "0 x 0";
console.log(arr);
arr.forEach(data=>{
if ("bitrate" in data){
@ -262,6 +263,12 @@
if (data.timestart){
document.getElementById("details").innerHTML += "<br /><b>Test start time:</b> "+timeConverter(data.timestart)+"<br />";
}
if ("peakhour" in data){
if (!data.peakhour){
document.getElementById("details").innerHTML += "<small><i>(This test was completed outside of peak streaming hours.</br>Results are normally worse during peak hours.)</i></small><br />";
}
}
if (data.summary){
if (data.summary.download && data.summary.upload){
@ -274,7 +281,12 @@
}
if ("resolution" in data){
updateData("resolution", data.resolution);
try {
if (parseInt(data.resolution.split("x ")[1])>parseInt(maxResolution.split("x ")[1])){
maxResolution = data.resolution;
}
} catch(e){}
//updateData("resolution", data.resolution);
}
if ("QLR" in data){
@ -310,6 +322,7 @@
if (data.info.CPU){
document.getElementById("details").innerHTML += "<br /><b>CPU:</b> "+data.info.CPU+"<br />";
}
if (data.info.conn_type){
document.getElementById("details").innerHTML += "<br /><b>Connection type:</b> "+data.info.conn_type+"<br />";
if (data.info.conn_type == "wifi"){
@ -318,9 +331,21 @@
}
}
if (data.encoder){
if (data.encoder.toLowerCase() == "libvpx"){
document.getElementById("details").innerHTML += "<br /><b>Default video codec used:</b> VP8<br />";
} else {
document.getElementById("details").innerHTML += "<br /><b>Default video codec used:</b> "+data.encoder.toUpperCase()+"<br />";
}
}
});
// container
if (maxResolution){
document.getElementById("details").innerHTML += "<br /><b>Highest video resolution reached:</b> "+maxResolution+"<br />";
}
var total = QLR_1 + QLR_2 + QLR_3;
if (QLR_2/total>0.5){
document.getElementById("container").innerHTML += "Serious CPU overload issues. Consider reducing the capture resolution.<br />";

View File

@ -94,12 +94,12 @@
<script>
function getChromeVersion() {
function getChromiumVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
//if (!getChromeVersion()){
//if (!getChromiumVersion()){
// alert("This speedtest is optimized for Chromium-based browsers; graphs will not work for Firefox or Safari browsers.");
//}

View File

@ -38,7 +38,7 @@
<script>
function getChromeVersion() {
function getChromiumVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
@ -64,7 +64,7 @@
}
if (!((safariVersion() && safariVersion()>=15) || (getChromeVersion() || getChromeVersion >=60))){
if (!((safariVersion() && safariVersion()>=15) || (getChromiumVersion() || getChromiumVersion >=60))){
alert("This tool really only works with recent Chromium based browsers; Firefox is not supported: "+ safariVersion());
}

View File

@ -554,34 +554,34 @@
"director-redirect-2": "Press OK to be redirected.",
"audio-processing-disabled": "Audio processing is disabled with this guest. Can't mute or change volume",
"not-the-director": "<font color='red'>You are not the director of this room. You will have limited to no control.</font>",
"room-is-claimed": "The room is already claimed by someone else.Only the first person to join a room is the assigned director.Refresh after the first director leaves to claim.",
"token-room-is-claimed": "The room is claimed by someone else.Join as a guest or co-director instead.",
"room-is-claimed-codirector": "The room is already claimed by someone else.Trying to join as a co-director...",
"streamid-already-published": "The stream ID you are publishing to is already in use.Please try with a different invite link or refresh to retry again.You will now be disconnected.",
"room-is-claimed": "The room is already claimed by someone else.\n\nOnly the first person to join a room is the assigned director.\n\nRefresh after the first director leaves to claim.",
"token-room-is-claimed": "The room is claimed by someone else.\n\nJoin as a guest or co-director instead.",
"room-is-claimed-codirector": "The room is already claimed by someone else.\n\nTrying to join as a co-director...",
"streamid-already-published": "The stream ID you are publishing to is already in use.\n\nPlease try with a different invite link or refresh to retry again.\n\nYou will now be disconnected.",
"director": "Director",
"unknown-user": "Unknown User",
"room-test-not-good": "The room name 'test' is very commonly used and may not be secure.Are you sure you wish to proceed?",
"room-test-not-good": "The room name 'test' is very commonly used and may not be secure.\n\nAre you sure you wish to proceed?",
"load-previous-session": "Would you like to load your previous session's settings?",
"enter-password": "Please enter the password below: (Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)",
"enter-password-2": "Please enter the password below: (Note: Passwords are case-sensitive.)",
"enter-director-password": "Please enter the director's password:(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)",
"password-incorrect": "The password was incorrect.Refresh and try again.",
"enter-password": "Please enter the password below: \n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)",
"enter-password-2": "Please enter the password below: \n\n(Note: Passwords are case-sensitive.)",
"enter-director-password": "Please enter the director's password:\n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)",
"password-incorrect": "The password was incorrect.\n\nRefresh and try again.",
"enter-display-name": "Please enter your display name:",
"enter-new-display-name": "Enter a new Display Name for this stream",
"what-bitrate": "What bitrate would you like to record at? (kbps)(note: This feature is experimental, so have backup recordings going)",
"what-bitrate": "What bitrate would you like to record at? (kbps)\n(note: This feature is experimental, so have backup recordings going)",
"enter-website": "Enter a website URL to share",
"press-ok-to-record": "Press OK to start recording. Press again to stop and download.Warning: Keep this browser tab active to continue recording.You can change the default video bitrate if desired below (kbps)",
"no-streamID-provided": "No streamID was provided; one will be generated randomily.Stream ID: ",
"alphanumeric-only": "Info: Only AlphaNumeric characters should be used for the stream ID.The offending characters have been replaced by an underscore",
"stream-id-too-long": "The Stream ID should be less than 45 alPhaNuMeric characters long.We will trim it to length.",
"press-ok-to-record": "Press OK to start recording. Press again to stop and download.\n\nWarning: Keep this browser tab active to continue recording.\n\nYou can change the default video bitrate if desired below (kbps)",
"no-streamID-provided": "No streamID was provided; one will be generated randomily.\n\nStream ID: ",
"alphanumeric-only": "Info: Only AlphaNumeric characters should be used for the stream ID.\n\nThe offending characters have been replaced by an underscore",
"stream-id-too-long": "The Stream ID should be less than 45 alPhaNuMeric characters long.\n\nWe will trim it to length.",
"share-with-trusted": "Share only with those you trust",
"pass-recommended": "A password is recommended",
"insecure-room-name": "Insecure room name.",
"allowed-chars": "Allowed chars",
"transfer": "transfer",
"armed": "armed",
"transfer-guest-to-room": "Transfer guests to room:(Please note rooms must share the same password)",
"transfer-guest-to-url": "Transfer guests to new website URL.Guests will be prompted to accept unless they are using &consent",
"transfer-guest-to-room": "Transfer guests to room:\n\n(Please note: rooms must share the same password)",
"transfer-guest-to-url": "Transfer guests to new website URL.\n\nGuests will be prompted to accept unless they are using &consent",
"mute-in-scene": "mute in scene",
"unmute-guest": "unmute guest",
"deafen": "deafen guest",
@ -606,15 +606,15 @@
"camera-tip-c922": "<i>Tip:</i> To achieve 60-fps with a C922 webcam, low-light compensation needs to be turned off, exposure set to auto, and 720p used.",
"camera-tip-camlink": "<i>Tip:</i> A Cam Link may glitch green/purple if accessed elsewhere while already in use.",
"samsung-a-series": "Samsung A-series phones may have issues with Chrome; if so, try Firefox Mobile instead or switch video codecs.",
"screen-permissions-denied": "Permission to capture denied. Ensure your browser has screen record system permissions1.On your Mac, choose Apple menu > System Preferences, click Security & Privacy , then click Privacy.2.Select Screen Recording.3.Select the checkbox next to your browser to allow it to record your screen.",
"change-audio-output-device": "Audio could not be captured. Please make sure you have an audio output device available.Some gaming headsets (ie: Corsair) may need to be set to 2-channel output to work, as surround sound drivers may cause problems.",
"screen-permissions-denied": "Permission to capture denied. Ensure your browser has screen record system permissions\n\n1.On your Mac, choose Apple menu > System Preferences, click Security & Privacy , then click Privacy.\n2.Select Screen Recording.\n3.Select the checkbox next to your browser to allow it to record your screen.",
"change-audio-output-device": "Audio could not be captured. Please make sure you have an audio output device available.\n\nSome gaming headsets (ie: Corsair) may need to be set to 2-channel output to work, as surround sound drivers may cause problems.",
"prompt-access-request": " is trying to view your stream. Allow them?",
"confirm-reload-user": "Are you sure you wish to reload this user's browser?",
"webrtc-is-blocked": "⚠ This browser has either blocked WebRTC or does not support it.This site will not work without it.Disable any browser extensions or privacy settings that may be blocking WebRTC, or try a different browser.",
"not-clean-session": "Video effects or canvas rendering failed.Check to ensure any remotely hosted images are cross-origin allowed.",
"webrtc-is-blocked": "⚠ This browser has either blocked WebRTC or does not support it.\n\nThis site will not work without it.\n\nDisable any browser extensions or privacy settings that may be blocking WebRTC, or try a different browser.",
"not-clean-session": "Video effects or canvas rendering failed.\n\nCheck to ensure any remotely hosted images are cross-origin allowed.",
"ios-no-screen-share": "Sorry, but your iOS browser does not support screen-sharing.",
"android-no-screen-share": "Sorry, your mobile browser does not support screen-sharing.",
"no-screen-share-supported": "Sorry, your browser does not support screen-sharing.Please use the desktop versions of Firefox or Chrome instead.",
"no-screen-share-supported": "Sorry, your browser does not support screen-sharing.\n\nPlease use the desktop versions of Firefox or Chrome instead.",
"speech-not-suppoted": "⚠ Speech Recognition is not supported by this browser",
"blue-yeti-tip": "<i>Tip:</i> Blue Yeti microphones may experience issues being overly loud. <a href='https://support.google.com/chrome/thread/7542181?hl=en&msgid=79691143'>Please see here</a> for a solution or disable auto-gain.",
"site-not-responsive": "<h3>Notice: The system cannot be accessed or is currently slow to respond.</h3>Check your connection or contact support.This service requires the use of Websockets over port 443.",
@ -623,12 +623,12 @@
"enter-url-for-widget": "Enter a URL for a page to embed as a sidebar",
"director-password": "Enter the main director's password",
"vision-disabled": "The Director has disabled your vision temporarily<br /><br ><center><i style='font-size:500%;' class='las la-eye-slash'></i></center>",
"invalid-remote-code": "Invalid remote control code.Use the field below to try again with a different passcode.",
"invalid-remote-code-obs": "Invalid remote control code.The remote OBS system needs a matching passcode set using &remote.See the documentation for help..",
"invalid-remote-code": "Invalid remote control code.\n\nUse the field below to try again with a different passcode.",
"invalid-remote-code-obs": "Invalid remote control code.\n\nThe remote OBS system needs a matching passcode set using &remote.\n\nSee the documentation for help..",
"request-rejected-obs": "The request was rejected.The remote OBS system needs a matching passcode set using &remote.See the documentation for help.",
"remote-token-rejected": "The remote request failed; the &remote token did not match or the remote user does not allow remote control.",
"remote-control-failed": "The remote control request failed.",
"remote-peer-connected": "Remote peer connected to video stream.Connection to handshake server being killed on request. This increases security, but the peer will not be able to reconnect automatically on connection failure.Press OK to start the stream!",
"remote-peer-connected": "Remote peer connected to video stream.\n\nConnection to handshake server being killed on request. This increases security, but the peer will not be able to reconnect automatically on connection failure.\n\nPress OK to start the stream!",
"director-denied": "The main director denied you as a co-director",
"only-main-director": "Only the main director can transfer this guest",
"request-failed": "The request failed; you can't apply this action",

View File

@ -1,3 +1,25 @@
/* function getAllContentNodes(element) { // takes an element.
element.childNodes.forEach(node=>{
if (node.childNodes.length){
if (node.dataset.translate){return;}
getAllContentNodes(node)
} else if ((node.nodeType === 3) && node.textContent && (node.textContent.trim().length > 0)){
var datatag = node.textContent.toLowerCase().replace(/[^a-zA-Z0-9\s\-]/g, '').trim().replaceAll(" ","-");
if (datatag){
var newNode = document.createElement("span");
newNode.dataset.translate = datatag;
newNode.innerHTML = node.textContent;
node.parentNode.replaceChild(newNode, node);
}
}
});
}
getAllContentNodes(document.body)
*/
// Copy and paste this code into VDO.Ninja's developer's console to generate new Translation files
function downloadTranslation(filename, trans = {}) { // downloads the current translation to a file

File diff suppressed because one or more lines are too long