From 4a203a1e604fb4a139b84eaaf3adf2f9a5f7520c Mon Sep 17 00:00:00 2001 From: Steve Seguin Date: Wed, 27 Jan 2021 10:27:45 -0500 Subject: [PATCH] Add files via upload --- README.md | 24 +- dock.html | 2 +- iframe.html | 5 + index.html | 160 +- main.css | 55 +- main.js | 10998 ++++++++++++++++++------------- media/cap.webm | Bin 0 -> 202578 bytes media/favicon-16x16.png | Bin 0 -> 757 bytes media/favicon-32x32.png | Bin 0 -> 1279 bytes media/favicon.ico | Bin 0 -> 15086 bytes media/hd.svg | 2 + media/icon.png | Bin 0 -> 45734 bytes media/obsNinja_logo_full.png | Bin 0 -> 305754 bytes media/old_logo.png | Bin 0 -> 16606 bytes media/robot.mp3 | Bin 0 -> 8459 bytes media/screenshare.webm | Bin 0 -> 89550 bytes media/sd.svg | 2 + media/share.jpg | Bin 0 -> 103700 bytes media/tone.mp3 | Bin 0 -> 2715 bytes media/tone.ogg | Bin 0 -> 6744 bytes thirdparty/adapter.min.js | 10 + thirdparty/polyfill.min.js.map | 1 + thirdparty/webmidi.js | 31 + 23 files changed, 6652 insertions(+), 4638 deletions(-) create mode 100644 media/cap.webm create mode 100644 media/favicon-16x16.png create mode 100644 media/favicon-32x32.png create mode 100644 media/favicon.ico create mode 100644 media/hd.svg create mode 100644 media/icon.png create mode 100644 media/obsNinja_logo_full.png create mode 100644 media/old_logo.png create mode 100644 media/robot.mp3 create mode 100644 media/screenshare.webm create mode 100644 media/sd.svg create mode 100644 media/share.jpg create mode 100644 media/tone.mp3 create mode 100644 media/tone.ogg create mode 100644 thirdparty/adapter.min.js create mode 100644 thirdparty/polyfill.min.js.map create mode 100644 thirdparty/webmidi.js diff --git a/README.md b/README.md index 36d8dcd..cad2e9e 100644 --- a/README.md +++ b/README.md @@ -14,32 +14,30 @@ Also check out the FAQ for more info: https://github.com/steveseguin/obsninja/wi ## How to use: I demo the basic usage of OBS.Ninja on YouTube: https://www.youtube.com/watch?v=6R_sQKxFAhg -Here is a podcast series showing how to use different basic OBS.Ninja features: https://www.youtube.com/watch?v=XfSqufuoV74&list=PLWodc2tCfAH1l_LDvEyxEqFf42hOBKqQM +Here is a podcast series showing how to use different basic OBS.Ninja features, including macOS support: https://www.youtube.com/watch?v=XfSqufuoV74&list=PLWodc2tCfAH1l_LDvEyxEqFf42hOBKqQM And Here is another video series touching on some more advanced settings: https://www.youtube.com/watch?v=mQ1Jdhf5aYg&list=PL8VJWj2-XLFpFu3G35Hdm1nKZ2xn9_0_8 Check the subreddit for added use cases, advanced features, and support. Advanced features includes high-quality audio modes, custom video resolutions, and more. -[Update as of January 2021] - -MacOS need to upgrade to OBS v26.1.2 or newer to have access native support for OBS.Ninja on macOS. Users with older OBS versions or using StreamLabs may still wish to use the Electron Capture app: https://github.com/steveseguin/electroncapture +MacOS users will face some challenges in using OBS 25/26, but there are workarounds. Please see the subreddit or the Wiki. ## What's in this repo? -This repo contains client-side software for OBS.Ninja, including the HTML landing page for its Electron Capture app offering. A sample config file and instructions for setting up a TURN server (video relay server) is also provided. You may also find the Wiki for the project in this repo, which contains added information on how to use the software. The code provided is designed to allow for innovation, customization, white-labelling, and exploration. +This repo contains software for OBS.Ninja, including the HTML landing page for its Electron Capture app offering. A sample config file and instructions for setting up a TURN server (video relay server), is also provided. You may also find the Wiki for the project in this repo, which contains added information on how to use the software. ## How to Deploy this Repo: -To use, just download and host the files on a HTTPS-enabled webserver. You may want to hide the .html extensions within your HTTP server as well, else the generated links may not work. See [here](https://github.com/steveseguin/obsninja/blob/master/install.md) for added details and alternative install options. +To use, just download and host the files on a HTTPS-enabled webserver. You may want to hide the .html extensions within your HTTP server as well, else the generated links will not work. See [here](https://github.com/steveseguin/obsninja/blob/master/install.md) for added details and alternative install options. -Directions on how to deploy a TURN server are listed in the turnserver.md file. You may wish to do so, although not all use cases will need one. Only about ~10% of remote guests need them; those often connected via 4G LTE or those behind corporate firewall. While OBS.Ninja does host some TURN servers freely for OBS.Ninja users, they are quite expensive to operate and are not really for private deployment use. If you are deploying your own version of OBS.Ninja, I'd ask you use your own TURN servers instead, but I likely won't enforce this unless there is heavy abuse. +Directions on how to deploy a TURN server are listed in the turnserver.md file. You may wish to do so, although not all use cases will not need one. Only about 10% of remote guests, those often connected via 4G LTE, will require a TURN server. While OBS.Ninja does host some TURN servers, they are quite expensive to operate and not really for private deployment use. If you are deploying your own version of OBS.Ninja, I'd ask you use your own TURN servers instead. ## Server side / API software? -Since OBS.Ninja uses peer-to-peer technology, video connections are made directly between viewer and publisher in ~90% of cases. The remaining connections will likely have to happen over a TURN video relay server, hosted in the cloud. These servers ensure peer connection compatibility. Very few users will see any benefit of using a TURN server over a direct peer connection, but there are still cases it may be helpful or required to deploy your own. Details on how to deploy a TURN server are provided in the repo. +Since OBS.Ninja uses peer-2-peer technology, video connections are made directly between viewer and publisher in 90% of cases. Hosting a TURN server yourself may help improve performance, but less than 1% of users will see any benefit of this. Details on how to deploy a TURN server are provided. For those capable of hosting their own TURN server, that would be appreciated if possible, as TURN servers are the only real cost incurred by OBS.Ninja at present. (other than time, of course) -Other than TURN servers, OBS.Ninja also uses public STUN servers and a custom hosted handshake server. These are used to facilitate the initial setup of peer connections and are generally not required after a peer connection is established. These servers are free to access and use, even for private deployments. This repo does not include details on setting up a STUN servers and does not yet make available the handshake server code. +Other than TURN servers, OBS.Ninja also uses public STUN servers and a hosted handshake server. These are used to facilitate the initial setup of peer connections and are generally not required after a peer connection is established. These servers are free to access and use, even for private deployments. -Development builds of OBS.Ninja may include debugging software, but in-production releases have this removed. Double check to ensure debugging dependencies are disabled though before deployment, just to be safe. Please see the index.html header for any such dependencies. +Development builds of OBS.Ninja may include debugging software, but in-production releases have this removed. Double check to ensure "console.re" debugging is disabled before deployment, just to be safe. -A design goal of OBS.Ninja is to be serverless and we are like 99% of the way there. This design objective ensures OBS.Ninja can be offered for free, along with providing increased levels of security and privacy. +A design goal of OBS.Ninja is to be serverless and we are 99% of the way there. This design objective ensures OBS.Ninja can be offered for free, along with providing increased levels of security and privacy. ## Issues? problems? Not working? @@ -65,9 +63,9 @@ https://steves.app/ A browser-based studio solution and simplified alternative to OBS, with built-in OBS.Ninja functionality. It is a server-based approach to group interactions and live production. Steve Seguin is affiliated with StageTEn, yet StageTEN is not affiliated with OBS.Ninja. ## Privacy -I try to avoid data collection whenever possible and video streams are generally designed to be private, but use at your own risk. It is best to not share links created with OBS.Ninja with those you do not trust. I've provided instructions on how to deploy a TURN server if IP-address privacy is an issue for you, as they can be used to mask your IP address, along with some VPN services. See: turnserver.md +I try to avoid data collection whenever possible and video streams are generally designed to be private, but use at your own risk. It is best to not share links created with OBS.Ninja with those you do not trust. I've provided instructions on how to deploy a TURN server if IP-address privacy is an issue for you. See: turnserver.md -https://obs.ninja may unavoidably use cookies that are exempt from EU laws of requiring notice of their use; they are exempt as they are required and necessary for the technical function of the web service. Our webserver is cached by Cloudflare and it provides denial of server protection for the users of OBS.Ninja. +https://obs.ninja may unavoidably use cookies that are exempt from EU laws of requiring notice of their use; they are exempt as they are required and necessary for the technical functioning of the web service. Our webserver is cached by Cloudflare and it provides denial of server protection for the users of OBS.Ninja. Additional security features are being added weekly on request. Please ask about these options if added security and privacy are requirements for you. diff --git a/dock.html b/dock.html index e29e66a..2c910f0 100644 --- a/dock.html +++ b/dock.html @@ -292,7 +292,7 @@ document.addEventListener("dragstart", event => {
- +
\ No newline at end of file diff --git a/iframe.html b/iframe.html index da7c053..1b434d2 100644 --- a/iframe.html +++ b/iframe.html @@ -228,6 +228,11 @@ function loadIframe(){ // this is pretty important if you want to avoid camera button.onclick = function(){iframe.contentWindow.postMessage({"function":"previewWebcam"}, '*');}; // publishScreen iframeContainer.appendChild(button); + var button = document.createElement("button"); + button.innerHTML = "eval('alert(\"DANGERUS\")'"; + button.onclick = function(){iframe.contentWindow.postMessage({"function":"eval", "value":'alert(\"DANGERUS\")'}, '*');}; // publishScreen + iframeContainer.appendChild(button); + var button = document.createElement("button"); button.innerHTML = "Change Add Camera text"; button.onclick = function(){iframe.contentWindow.postMessage({"function":"changeHTML", "target":"add_camera", "value":"NEW CAMERA TEXT"}, '*');}; // change text of add camera button diff --git a/index.html b/index.html index 9bdf4be..7b05890 100644 --- a/index.html +++ b/index.html @@ -19,10 +19,10 @@ - - - - + + + + OBS.Ninja @@ -34,8 +34,8 @@ - - + + @@ -44,28 +44,30 @@ - + - - - - - - - - + + + + - + - + - + - + -
+

Create Reusable Invite

@@ -433,7 +447,7 @@

- +

@@ -502,11 +516,11 @@
Add a password: - +
Add the guest to a room: - +
- Unlock Max Video Bitrate + âš This can cause video playback to lag Unlock Video Bitrate
- - + + - +
+
- +
@@ -1120,7 +1145,7 @@

- Add More Here! + Add More Here!
@@ -1131,7 +1156,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 = "15.1"; + session.version = "15.3"; session.streamID = session.generateStreamID(); // randomly generates a streamID for this session. You can set your own programmatically if needed session.defaultPassword = "someEncryptionKey123"; // Disabling improves compatibility and is helpful for debugging. @@ -1158,7 +1183,7 @@ // session.configuration.iceTransportPolicy = "relay"; // uncomment to enable "&privacy" and force the TURN server - // session.wss = false; // uses default handshake wss + // session.wss = "wss://wss14.obs.ninja:443"; //false; // uses default handshake wss ///// The following lets you set the defaults @@ -1196,7 +1221,26 @@ --> - - + + diff --git a/main.css b/main.css index a95664b..bf4323f 100644 --- a/main.css +++ b/main.css @@ -139,6 +139,10 @@ button.grey { background-size: 50%; } +#reportbutton{ + visibility: hidden; +} + .red { background-color: #840000 !important; } @@ -450,30 +454,26 @@ button.glyphicon-button.active.focus { border-radius: 38px; background-color: #030303dd; padding: 5px 7px; - bottom: 2.25em; position: absolute; pointer-events: auto; } -@media only screen and (max-width: 500px){ +@media only screen and (max-width: 700px){ #subControlButtons { padding: 2px 4px; - zoom: .9; -moz-transform: scale(.9); } } @media only screen and (max-width: 400px){ #subControlButtons { padding: 1px 2px; - bottom: 2.1em; - zoom: .8; + zoom: .9; -moz-transform: scale(.8); } } @media only screen and (max-width: 300px){ #subControlButtons { padding: 0px; - bottom: 2.0em; } } @@ -853,7 +853,33 @@ input[type=range]:focus::-ms-fill-upper { text-shadow: 0px 0px 6px #000000FF; font-weight: 700; } - +.tooltip { + position: relative; + display: inline-block; + border-bottom: 1px dotted black; +} +.tooltip .tooltiptext { + visibility: hidden; + width: 10em; + background-color: #9d5050; + color: #fff; + text-align: center; + /* padding: 5px 0; */ + border-radius: 10px; + position: absolute; + z-index: 1; + top: -10px; + font-family: "Lato", sans-serif; +} +.tooltip:hover .tooltiptext { + visibility: visible; +} +#screensharebutton.float2{ + background-color: #335c3a; +} +#screenshare2button.float2{ + background-color: #335c3a; +} #popupSelector { background: linear-gradient(6deg, rgba(221, 221, 221, 0) 4%, rgb(221, 221, 221, 0.2) 30%, rgba(120, 120, 100, .5) 100%); transition: all 0.2s linear 0s; @@ -861,9 +887,10 @@ input[type=range]:focus::-ms-fill-upper { position: fixed; top: 0px; height: 90%; - width: 490px; + width: 505px; right: -400px; overflow: auto; + z-index: 1; } h2 { @@ -1141,7 +1168,7 @@ img { border: 0; pointer-events: none; } - +.popup .menu { margin: 2px; } .my-float { margin-top: 7px; opacity: 0.9; @@ -2166,6 +2193,16 @@ span#guestTips { border-radius: 1vh; pointer-events:none; } + +.video-mute-state { + top: 0.5em; + right: 0.5em; + position: absolute; + color:white; + border-radius: 1vh; + background-color:#b11313; +} + #help_directors_room{ cursor:pointer; } diff --git a/main.js b/main.js index e906d2e..867ed48 100644 --- a/main.js +++ b/main.js @@ -1,122 +1,162 @@ /* -* Copyright (c) 2020 Steve Seguin. All Rights Reserved. -* -* Use of this source code is governed by the APGLv3 open-source license -* that can be found in the LICENSE file in the root of the source -* tree. Alternative licencing options can be made available on request. -* -*/ + * Copyright (c) 2020 Steve Seguin. All Rights Reserved. + * + */ /*jshint esversion: 6 */ var formSubmitting = true; var activatedPreview = false; -function getById(id) { - var el = document.getElementById(id); - if (!el) { - warnlog(id + " is not defined; skipping."); +function getById(id) { // js helper + var el = document.getElementById(id); + if (!el) { + warnlog(id + " is not defined; skipping."); el = document.createElement("span"); // create a fake element - } - return el; + } + return el; } +function addEventToAll(targets, trigger, callback) { // js helper + const target = document.querySelectorAll(targets); + var triggers = trigger.split(" "); + for (let i = 0; i < target.length; i++) { + for (let j = 0; j < triggers.length; j++) { + target[i].addEventListener(triggers[j], function(e) { + callback(e, target[i]); + }); + } + } +} -function changeParam(url,paramName,paramValue){ +function mapToAll(targets, callback, parentElement = document) { // js helper + if (!targets) { + return; + } + if (!parentElement) { + return; + } + const target = parentElement.querySelectorAll(targets); + for (let i = 0; i < target.length; i++) { + callback(target[i]); + } +} + +var isIFrame = false; +if ( parent && (window.location !== window.parent.location )) { + isIFrame = true; +} + +function changeParam(url, paramName, paramValue) { var qind = url.indexOf('?'); - var params = url.substring(qind+1).split('&'); + var params = url.substring(qind + 1).split('&'); var query = ''; - for(var i=0;i 1 && tokens[1] !== ''){ + if (tokens.length > 1 && tokens[1] !== '') { var value = tokens[1]; - } else{ + } else { value = ""; } - + if (name == paramName) { + if (match) { + continue; + } // already matched the first time. + match = true; value = paramValue; } - if (value!==""){ - value = '=' + value; - } - - if(query == '') { - query = "?"+name + value; + if (value !== "") { + value = '=' + value; + } + + if (query == '') { + query = "?" + name + value; } else { query = query + '&' + name + value; } } - return url.substring(0,qind) + query; + return url.substring(0, qind) + query; } -function updateURL(param, force=false, cleanUrl=false){ +function updateURL(param, force = false, cleanUrl = false) { var para = param.split('='); - if (cleanUrl){ - if (history.pushState){ + if (cleanUrl) { + if (history.pushState) { var href = new URL(cleanUrl); - if (para.length==1){ - href = changeParam(cleanUrl, para[0],"") + if (para.length == 1) { + href = changeParam(cleanUrl, para[0], "") } else { - href = changeParam(cleanUrl, para[0],para[1]); + href = changeParam(cleanUrl, para[0], para[1]); } - log("--"+href.toString()); - window.history.pushState({path:href.toString()},'',href.toString()); + log("--" + href.toString()); + window.history.pushState({ + path: href.toString() + }, '', href.toString()); } - } else if (!(urlParams.has(para[0]))){ - if (history.pushState){ + } else if (!(urlParams.has(para[0]))) { // don't need to replace as it doesn't exist. + if (history.pushState) { var arr = window.location.href.split('?'); var newurl; if (arr.length > 1 && arr[1] !== '') { - newurl = window.location.href + '&' +param; + newurl = window.location.href + '&' + param; } else { - newurl = window.location.href + '?' +param; + updateURL + newurl = window.location.href + '?' + param; } - - window.history.pushState({path:newurl},'',newurl); + + window.history.pushState({ + path: newurl.toString() + }, '', newurl.toString()); } - } else if (force){ - if (history.pushState){ + } else if (force) { + if (history.pushState) { var href = new URL(window.location.href); - if (para.length==1){ - href = changeParam(window.location.href, para[0],"") + if (para.length == 1) { + href = changeParam(window.location.href, para[0], "") } else { href = changeParam(window.location.href, para[0], para[1]); } - log("---"+href.toString()); - window.history.pushState({path:href.toString()},'',href.toString()); + log("---" + href.toString()); + window.history.pushState({ + path: href.toString() + }, '', href.toString()); } } - if (session.sticky){ + if (session.sticky) { setCookie("settings", encodeURI(window.location.href), 90); } + urlParams = new URLSearchParams(window.location.search); } -var filename=false; -try{ - filename = window.location.pathname.substring(window.location.pathname.lastIndexOf('/')+1); +var filename = false; +try { + filename = window.location.pathname.substring(window.location.pathname.lastIndexOf('/') + 1); filename2 = filename.split("?")[0]; // split at ??? - if (filename.split(".").length==1){ - if (filename2.length<2){ // easy win - filename=false; - } else if (filename.startsWith("&")){ // easy win + if (filename.split(".").length == 1) { + if (filename2.length < 2) { // easy win + filename = false; + } else if (filename.startsWith("&")) { // easy win var tmpHref = window.location.href.substring(0, window.location.href.lastIndexOf('/')) + "/?" + filename.split("&").slice(1).join("&"); - log("TMP "+tmpHref); + log("TMP " + tmpHref); updateURL(filename.split("&")[1], true, tmpHref); - filename=false; - } else if (filename2.split("&")[0].includes("=")){ - log("asdf "+ filename.split("&")[0]); - if (history.pushState){ + filename = false; + } else if (filename2.split("&")[0].includes("=")) { + log("asdf " + filename.split("&")[0]); + if (history.pushState) { var tmpHref = window.location.href.substring(0, window.location.href.lastIndexOf('/')); - tmpHref = tmpHref+"/?"+filename; + tmpHref = tmpHref + "/?" + filename; filename = false; //alert("Please ensure your URL is correctly formatted."); - window.history.pushState({path:tmpHref.toString()},'',tmpHref.toString()); + window.history.pushState({ + path: tmpHref.toString() + }, '', tmpHref.toString()); } } else { filename = filename2.split("&")[0]; - if (filename2!=filename){ + if (filename2 != filename) { alert("Warning: Please ensure your URL is correctly formatted."); } } @@ -124,69 +164,70 @@ try{ filename = false; } log(filename); -} catch(e){errorlog(e);} +} catch (e) { + errorlog(e); +} -(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; - } - }; - }; +(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); -sanitizeStreamID = function(streamID){ +var sanitizeStreamID = function(streamID) { streamID = streamID.trim(); - - if (streamID.length < 1){ + + if (streamID.length < 1) { streamID = session.generateStreamID(8); - if (!(session.cleanOutput)){ - alert("No streamID was provided; one will be generated randomily.\n\nStream ID: "+streamID); + if (!(session.cleanOutput)) { + alert("No streamID was provided; one will be generated randomily.\n\nStream ID: " + streamID); } } - var streamID_sanitized = streamID.replace(/[\W]+/g,"_"); - if (streamID !== streamID_sanitized){ - if (!(session.cleanOutput)){ + var streamID_sanitized = streamID.replace(/[\W]+/g, "_"); + if (streamID !== streamID_sanitized) { + if (!(session.cleanOutput)) { alert("Info: Only AlphaNumeric characters should be used for the stream ID.\n\nThe offending characters have been replaced by an underscore"); } } - if (streamID_sanitized.length > 24){ + if (streamID_sanitized.length > 24) { streamID_sanitized = streamID_sanitized.substring(0, 24); - if (!(session.cleanOutput)){ + if (!(session.cleanOutput)) { alert("The Stream ID should be less than 25 alPhaNuMeric characters long.\n\nWe will trim it to length."); } } return streamID_sanitized; }; -checkStrength = function(string){ +var checkStrength = function(string) { var matcher = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{7,30}$/; - if (string.match(matcher)){ + if (string.match(matcher)) { return true; - } else if (string.length>20){ + } else if (string.length > 20) { return true; } else { return false; } } -checkStrengthRoom = function(){ +var checkStrengthRoom = function() { var result1 = checkStrength(getById('videoname1').value); var result2 = getById('passwordRoom').value.length; var target = getById('securityLevelRoom'); - target.style.display="block"; - if (result1){ - if (result2){ + target.style.display = "block"; + if (result1) { + if (result2) { target.innerHTML = "Share only with those you trust"; } else { target.innerHTML = "A password is recommended"; @@ -196,7 +237,7 @@ checkStrengthRoom = function(){ } } -sanitizeChat = function(string){ +var sanitizeChat = function(string) { var temp = document.createElement('div'); temp.innerText = string; temp.innerText = temp.innerHTML; @@ -205,159 +246,184 @@ sanitizeChat = function(string){ return temp.trim(); } -sanitizeString = function(str){ - str = str.replace(/[^a-z0-9áéíóúñü \.,_-]/gim,""); - return str.trim(); +var sanitizeString = function(str) { + str = str.replace(/[^a-z0-9áéíóúñü \.,_-]/gim, ""); + return str.trim(); } -sanitizeLabel = function(string){ - let temp = document.createElement("div"); - temp.innerText = string; - temp.innerText = temp.innerHTML; - temp = temp.textContent || temp.innerText || ""; - temp = temp.substring(0, Math.min(temp.length, 50)); - return temp.trim() +var sanitizeLabel = function(string) { + let temp = document.createElement("div"); + temp.innerText = string; + temp.innerText = temp.innerHTML; + temp = temp.textContent || temp.innerText || ""; + temp = temp.substring(0, Math.min(temp.length, 50)); + return temp.trim() } -sanitizeRoomName = function(roomid){ +var sanitizeRoomName = function(roomid) { roomid = roomid.trim(); - if (roomid===""){return roomid;} - else if (roomid===false){return roomid;} - - var sanitized = roomid.replace(/[\W]+/g,"_"); - if (sanitized!==roomid){ - if (!(session.cleanOutput)){ + if (roomid === "") { + return roomid; + } else if (roomid === false) { + return roomid; + } + + var sanitized = roomid.replace(/[\W]+/g, "_"); + if (sanitized !== roomid) { + if (!(session.cleanOutput)) { alert("Info: Only AlphaNumeric characters should be used for the room name.\n\nThe offending characters have been replaced by an underscore"); } } - if (sanitized.length > 30){ + if (sanitized.length > 30) { sanitized = sanitized.substring(0, 30); - if (!(session.cleanOutput)){ + if (!(session.cleanOutput)) { alert("The Room name should be less than 31 alPhaNuMeric characters long.\n\nWe will trim it to length."); } - } + } return sanitized; }; -sanitizePassword = function(passwrd){ - if (passwrd===""){return passwrd;} - else if (passwrd===false){return passwrd;} - else if (passwrd===null){return passwrd;} +var sanitizePassword = function(passwrd) { + if (passwrd === "") { + return passwrd; + } else if (passwrd === false) { + return passwrd; + } else if (passwrd === null) { + return passwrd; + } passwrd = passwrd.trim(); - if (passwrd.length < 1){ - if (!(session.cleanOutput)){ + if (passwrd.length < 1) { + if (!(session.cleanOutput)) { alert("The password provided was blank."); } } - var sanitized = passwrd.replace(/[\W]+/g,"_"); - if (sanitized!==passwrd){ - if (!(session.cleanOutput)){ + var sanitized = passwrd.replace(/[\W]+/g, "_"); + if (sanitized !== passwrd) { + if (!(session.cleanOutput)) { alert("Info: Only AlphaNumeric characters should be used in the password.\n\nThe offending characters have been replaced by an underscore"); } } return sanitized; }; -function getChromeVersion(){ - var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); - return raw ? parseInt(raw[2], 10) : false; +function getChromeVersion() { + var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + return raw ? parseInt(raw[2], 10) : false; } -function safariVersion(){ +function safariVersion() { try { var ver = navigator.appVersion.split("Version/"); - if (ver.length>1){ + if (ver.length > 1) { ver = ver[1].split(" Safari"); } - if (ver.length>1){ + if (ver.length > 1) { ver = ver[0].split("."); } - if (ver.length>1){ + if (ver.length > 1) { ver = parseInt(ver[0]); } else { ver = 0; } - } catch(e){return 0;} + } catch (e) { + return 0; + } return ver; } -if (window.obsstudio){ +if (urlParams.has('optimize')) { + var optimize = parseInt(urlParams.get('optimize')); + if (!optimize) { + optimize = 600; + } + session.optimize = optimize; +} + +if (window.obsstudio) { session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? session.audioMeterGuest = false; - session.audioEffects = false; - session.obsfix=15; // can be manually set via URL. ; VP8=15, VP9=30. (previous was 20.) - try{ - log("OBS VERSION:"+window.obsstudio.pluginVersion); - log("macOS: "+navigator.userAgent.indexOf('Mac OS X') != -1); + session.audioEffects = false; + session.obsfix = 15; // can be manually set via URL. ; VP8=15, VP9=30. (previous was 20.) + try { + log("OBS VERSION:" + window.obsstudio.pluginVersion); + log("macOS: " + navigator.userAgent.indexOf('Mac OS X') != -1); log(window.obsstudio); - - if (!(urlParams.has('streamlabs'))){ - + + if (!(urlParams.has('streamlabs'))) { + var ver = window.obsstudio.pluginVersion; ver1 = ver.split("."); updateURL("streamlabs"); var cefVersion = getChromeVersion(); - - if (ver1.length == 3){ // Should be 3, but disabled3 - if ((ver1.length == 3) && (parseInt(ver1[0])==2) && ( cefVersion<76 ) && (navigator.userAgent.indexOf('Mac OS X') != -1)){ + + if (ver1.length == 3) { // Should be 3, but disabled3 + if ((ver1.length == 3) && (parseInt(ver1[0]) == 2) && (cefVersion < 76) && (navigator.userAgent.indexOf('Mac OS X') != -1)) { getById("main").innerHTML = "

Update OBS Studio to v26.1.2 or newer; older versions not supported.\
download here: https://github.com/obsproject/obs-studio/releases/tag/26.1.2\



\

Please use the Electron Capture app if there are further problems.

\
You can find more details within our wiki guide - https://github.com/steveseguin/obsninja/wiki/FAQ#mac-os\ -
If using OBS v23 or Streamlabs, you can bypass this error message by refreshing, Clicking Here, or by adding &streamlabs to the URL.\ +
If using OBS v23 or Streamlabs, you can bypass this error message by refreshing, Clicking Here, or by adding &streamlabs to the URL.\ \
Please report this problem to steve@seguin.email if you feel it is an error.\
"; } } } - } catch(e){errorlog(e);} - - window.addEventListener('obsSceneChanged', function(event){ + } catch (e) { + errorlog(e); + } + + //if (session.optimize){ + if (navigator.userAgent.indexOf('Mac OS X') != -1) { + session.codec = "h264"; // default the codec to h264 if OBS and macOS + } + //} + + window.addEventListener('obsSceneChanged', function(event) { log("OBS EVENT"); log(event.detail.name); - + window.obsstudio.getCurrentScene(function(scene) { log("OBS SCENE"); log(scene); }); - - window.obsstudio.getStatus(function (status) { + + window.obsstudio.getStatus(function(status) { log("OBS STATUS:"); log(status); }); }); - + } -window.onload = function() { // This just keeps people from killing the live stream accidentally. Also give me a headsup that the stream is ending - window.addEventListener("beforeunload", function (e) { - try{ +window.onload = function winonLoad() { // This just keeps people from killing the live stream accidentally. Also give me a headsup that the stream is ending + window.addEventListener("beforeunload", function(e) { + try { session.ws.close(); - if (session.videoElement.recording){ + if (session.videoElement.recording) { session.videoElement.recorder.writer.close(); - session.videoElement.recording=false; + session.videoElement.recording = false; } - for (i in session.rpcs){ - if (session.rpcs[i].videoElement){ - if (session.rpcs[i].videoElement.recording){ + for (i in session.rpcs) { + if (session.rpcs[i].videoElement) { + if (session.rpcs[i].videoElement.recording) { session.rpcs[i].videoElement.recorder.writer.close(); - session.rpcs[i].videoElement.recording=false; + session.rpcs[i].videoElement.recording = false; } } } - } catch (e){} + } catch (e) {} //setTimeout(function(){session.hangup();},0); return undefined; // ADDED OCT 29th; get rid of popup. Just close the socket connection if the user is refreshing the page. It's one or the other. - + }); }; -getById("credits").innerHTML = "Version: "+session.version+" - "+getById("credits").innerHTML; +getById("credits").innerHTML = "Version: " + session.version + " - " + getById("credits").innerHTML; var lastTouchEnd = 0; -document.addEventListener('touchend', function (event) { +document.addEventListener('touchend', function(event) { var now = (new Date()).getTime(); if (now - lastTouchEnd <= 300) { event.preventDefault(); @@ -366,72 +432,72 @@ document.addEventListener('touchend', function (event) { }, false); -document.addEventListener('click', function (event) { - if (session.firstPlayTriggered==false){ +document.addEventListener('click', function(event) { + if (session.firstPlayTriggered == false) { playAllVideos(); - session.firstPlayTriggered=true; + session.firstPlayTriggered = true; history.pushState({}, ''); } }); var Callbacks = []; var CtrlPressed = false; // global -var AltPressed = false; -document.addEventListener("keydown", event => { +var AltPressed = false; +document.addEventListener("keydown", event => { - if ((event.ctrlKey) || (event.metaKey) ){ // detect if CTRL is pressed + if ((event.ctrlKey) || (event.metaKey)) { // detect if CTRL is pressed CtrlPressed = true; } else { CtrlPressed = false; } - if (event.altKey){ + if (event.altKey) { AltPressed = true; } else { AltPressed = false; } - - - if (CtrlPressed && event.keyCode){ - - if (event.keyCode == 77) { // m - if (event.metaKey){ - if (AltPressed){ - toggleMute(); // macOS + + + if (CtrlPressed && event.keyCode) { + + if (event.keyCode == 77) { // m + if (event.metaKey) { + if (AltPressed) { + toggleMute(); // macOS + } + } else { + toggleMute(); // Windows } - } else { - toggleMute(); // Windows + // } else if (event.keyCode == 69) { // e + // hangup(); + } else if (event.keyCode == 66) { // b + toggleVideoMute(); } - // } else if (event.keyCode == 69) { // e - // hangup(); - } else if (event.keyCode == 66) { // b - toggleVideoMute(); - } } - - + + }); document.addEventListener("keyup", event => { - if (!((event.ctrlKey) || (event.metaKey))){ - if (CtrlPressed){ + if (!((event.ctrlKey) || (event.metaKey))) { + if (CtrlPressed) { CtrlPressed = false; - for (var i in Callbacks){ + for (var i in Callbacks) { var cb = Callbacks[i]; log(cb.slice(1)); cb[0](...cb.slice(1)); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#A_better_apply } - Callbacks=[]; + Callbacks = []; } } - if (!(event.altKey)){ + if (!(event.altKey)) { AltPressed = false; } }); window.onpopstate = function() { - if (session.firstPlayTriggered){ + if (session.firstPlayTriggered) { window.location.reload(true); } -}; +}; if (typeof session === 'undefined') { // make sure to init the WebRTC if not exists. var session = WebRTC.Media; @@ -440,29 +506,32 @@ if (typeof session === 'undefined') { // make sure to init the WebRTC if not exi } function makeDraggableElement(elmnt) { - try{ + try { elmnt.dragElement = false; elmnt.style.bottom = "auto"; - elmnt.style.cursor="grab"; + elmnt.style.cursor = "grab"; elmnt.stashonmouseup = null; elmnt.stashonmousemove = null; - - } catch(e){ + + } catch (e) { return; }; - - var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + + var pos1 = 0 + , pos2 = 0 + , pos3 = 0 + , pos4 = 0; elmnt.onmousedown = dragMouseDown; - + function dragMouseDown(e) { e = e || window.event; e.preventDefault(); - + pos3 = e.clientX; pos4 = e.clientY; elmnt.stashonmouseup = document.onmouseup; // I don't want to interfere with other drag events. elmnt.stashonmousemove = document.onmousemove; - + document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } @@ -475,9 +544,11 @@ function makeDraggableElement(elmnt) { pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; - - var topDrag = (elmnt.offsetTop - pos2); - if (topDrag>-3){topDrag=-3}; + + var topDrag = (elmnt.offsetTop - pos2); + if (topDrag > -3) { + topDrag = -3 + }; elmnt.style.top = topDrag + "px"; elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; } @@ -489,33 +560,33 @@ function makeDraggableElement(elmnt) { } function setCookie(cname, cvalue, exdays) { - var d = new Date(); - d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); - var expires = "expires="+d.toUTCString(); - document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; + var d = new Date(); + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + var expires = "expires=" + d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; } function getCookie(cname) { - var name = cname + "="; - var ca = document.cookie.split(';'); - for(var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1); - } - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - return ""; + var name = cname + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return ""; } -if (getCookie("redirect") == "yes"){ +if (getCookie("redirect") == "yes") { setCookie("redirect", "", 0); session.sticky = true; -} else if (getCookie("settings") != ""){ +} else if (getCookie("settings") != "") { session.sticky = confirm("Would you like you load your previous session's settings?"); - if (!session.sticky){ + if (!session.sticky) { setCookie("settings", "", 0); log("deleting cookie as user said no"); } else { @@ -524,136 +595,207 @@ if (getCookie("redirect") == "yes"){ window.location.replace(cookieSettings); } } -if (urlParams.has('sticky')){ - if (getCookie("permission")==""){ +if (urlParams.has('sticky')) { + if (getCookie("permission") == "") { session.sticky = confirm("Would you allow us to store a cookie to keep your session settings persistent?"); } else { session.sticky = true; } - if (session.sticky){ + if (session.sticky) { setCookie("permission", "yes", 999); setCookie("settings", encodeURI(window.location.href), 90); } } -if (urlParams.has('retrytimeout')){ +if (urlParams.has('retrytimeout')) { session.retryTimeout = parseInt(urlParams.get('retrytimeout')); } -var screensharebutton=true; -if (urlParams.has('nosettings') || urlParams.has('ns')){ - screensharebutton=false; +var screensharebutton = true; +var screensharesupport = true; +if (urlParams.has('nosettings') || urlParams.has('ns')) { + screensharebutton = false; session.showSettings = false; } -if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { + +if (urlParams.has('screenshareid') || urlParams.has('ssid')) { + if (urlParams.get('screenshareid') || urlParams.get('ssid')) { + session.screenshareid = urlParams.get('screenshareid') || urlParams.get('ssid'); + session.screenshareid = sanitizeStreamID(session.screenshareid); + } +} + +if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { //session.webcamonly = true; - getById("shareScreenGear").style.display="none"; - screensharebutton=false; + getById("shareScreenGear").style.display = "none"; + screensharebutton = false; + screensharesupport = false; getById("container-2").className = 'column columnfade advanced'; // Hide screen share on mobile - getById("dropButton").style.display="none"; + getById("dropButton").style.display = "none"; //session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? session.audioEffects = false; // disable audio inbound effects also. session.audioMeterGuest = false; -} else if ((iOS) || (iPad)){ - getById("shareScreenGear").style.display="none"; - screensharebutton=false; +} else if ((iOS) || (iPad)) { + getById("shareScreenGear").style.display = "none"; + screensharebutton = false; + screensharesupport = false; getById("container-2").className = 'column columnfade advanced'; // Hide screen share on mobile - getById("dropButton").style.display="none"; + getById("dropButton").style.display = "none"; //session.audiobitrate = false; // iOS devices seem to get distortion with custom audio bitrates. Disable for now. - session.maxiosbitrate = 10; // this is 10-kbps by default already. + //session.maxiosbitrate = 10; // this is 10-kbps by default already. //session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? session.audioEffects = false; // disable audio inbound effects also. - session.audioMeterGuest=false; + session.audioMeterGuest = false; } else { makeDraggableElement(document.getElementById("subControlButtons")); } -if (/CriOS/i.test(navigator.userAgent) && /iphone|ipod|ipad/i.test(navigator.userAgent)){ - alert("Chrome on iPhones/iPads do not support the required technology to use this site.\n\nPlease use Safari instead."); +if (/CriOS/i.test(navigator.userAgent) && (iOS || iPad)) { + if (!(session.cleanOutput)) { + try { + navigator.mediaDevices.getUserMedia; + } catch (e) { + alert("Chrome on this device does not support the required technology to use this site.\n\nPlease use Safari instead or update your iOS and browser version."); + }; + } } -if (urlParams.has('broadcast') || urlParams.has('bc')){ +if (urlParams.has('broadcast') || urlParams.has('bc')) { log("Broadcast flag set"); session.broadcast = urlParams.get('broadcast') || urlParams.get('bc'); - if ((iOS) || (iPad)){ - session.nopreview=false; + if ((iOS) || (iPad)) { + session.nopreview = false; } else { - session.nopreview=true; + session.nopreview = true; } - session.style=1; - getById("header").style.display="none"; + session.style = 1; + getById("header").style.display = "none"; getById("header").style.opacity = 0; } var directorLanding = false; -if (urlParams.has('director')){ +if (urlParams.has('director')) { directorLanding = urlParams.get('director'); - if (directorLanding===null){ + if (directorLanding === null) { directorLanding = true; - } else if (directorLanding.length===0){ + } else if (directorLanding.length === 0) { directorLanding = true; } else { directorLanding = false; } -} else if (filename==="director"){ - directorLanding=true; - filename=false; +} else if (filename === "director") { + directorLanding = true; + filename = false; } -if (urlParams.has('webcam') || urlParams.has('wc')){ +if (urlParams.has('showdirector')) { + session.showDirector = true; +} + +if (urlParams.has('midi') || urlParams.has('hotkeys')) { + session.midiHotkeys = true; +} +var loadedQRCode = false; +function loadQR(){ + if (loadedQRCode==false){ + loadedQRCode=true; + var script = document.createElement('script'); + script.src = "./thirdparty/qrcode.min.js"; // dynamically load this only if its needed. Keeps loading time down. + document.head.appendChild(script); + } +} + +if (urlParams.has('webcam') || urlParams.has('wc')) { session.webcamonly = true; - screensharebutton=false; -} else if (urlParams.has('screenshare') || urlParams.has('ss')){ + screensharebutton = false; +} else if (urlParams.has('screenshare') || urlParams.has('ss')) { session.screenshare = true; -} else if (urlParams.has('fileshare') || urlParams.has('fs')){ +} else if (urlParams.has('fileshare') || urlParams.has('fs')) { getById("container-5").classList.remove('advanced'); getById("container-5").classList.add("skip-animation"); getById("container-5").classList.remove('pointer'); -} else if (directorLanding){ +} else if (directorLanding) { getById("container-1").classList.remove('advanced'); getById("container-1").classList.add("skip-animation"); getById("container-1").classList.remove('pointer'); -} else if (urlParams.has('website') || urlParams.has('iframe')){ +} else if (urlParams.has('website') || urlParams.has('iframe')) { getById("container-6").classList.remove('advanced'); getById("container-6").classList.add("skip-animation"); getById("container-6").classList.remove('pointer'); } -if (urlParams.has('ssb')){ - screensharebutton=true; +if (urlParams.has('ssb')) { + screensharebutton = true; } -if (urlParams.has('mute') || urlParams.has('muted') || urlParams.has('m')){ +if (urlParams.has('mute') || urlParams.has('muted') || urlParams.has('m')) { session.muted = true; } -if (session.screenshare==true){ +if (urlParams.has('speakermute') || urlParams.has('mutespeaker') || urlParams.has('sm') || urlParams.has('ms')) { + session.speakerMuted = true; + getById("mutespeakertoggle").className = "las la-volume-mute my-float toggleSize"; + //getById("mutespeakerbutton").className="advanced float2 red"; + getById("mutespeakerbutton").classList.add("red"); + getById("mutespeakerbutton").classList.add("float2"); + getById("mutespeakerbutton").classList.remove("float"); + + var sounds = document.getElementsByTagName("video"); + for (var i = 0; i < sounds.length; ++i) { + sounds[i].muted = session.speakerMuted; + } +} + +if (urlParams.has('chatbutton') || urlParams.has('chat') || urlParams.has('cb')) { + session.chatbutton = urlParams.get('chatbutton') || urlParams.get('chat') || urlParams.get('cb'); + if (session.chatbutton === "false") { + session.chatbutton = false; + } else if (session.chatbutton === "0") { + session.chatbutton = false; + } else if (session.chatbutton === "no") { + session.chatbutton = false; + } else if (session.chatbutton === "off") { + session.chatbutton = false; + } else { + session.chatbutton = true; + getById("chatbutton").classList.remove("advanced"); + getById("controlButtons").style.display = "inherit"; + } +} + +if (session.screenshare == true) { getById("container-3").className = 'column columnfade advanced'; // Hide screen share on mobile getById("container-2").classList.add("skip-animation"); getById("container-2").classList.remove('pointer'); } -if (urlParams.has('manual')){ +if (urlParams.has('manual')) { session.manual = true; } -if (urlParams.has('hands') || urlParams.has('hand')){ +if (urlParams.has('hands') || urlParams.has('hand')) { session.raisehands = true; } -if (urlParams.has('record')){ - if (safariVersion()){ - if (!(session.cleanOutput)){ +if (urlParams.has('portrait') || urlParams.has('916') || urlParams.has('vertical')) { + session.aspectratio = 1; // 9:16 (default of 0 is 16:9) +} else if (urlParams.has('square') || urlParams.has('11')) { + session.aspectratio = 2; // 9:16 (default of 0 is 16:9) +} + +if (urlParams.has('record')) { + if (safariVersion()) { + if (!(session.cleanOutput)) { alert("Your browser or device is not supported. Try Chrome if on macOS."); } } else { session.recordLocal = urlParams.get('record'); - - if (session.recordLocal!== parseInt(session.recordLocal)){ + + if (session.recordLocal !== parseInt(session.recordLocal)) { session.recordLocal = 6000; } else { session.recordLocal = parseInt(session.recordLocal); @@ -661,89 +803,91 @@ if (urlParams.has('record')){ } } -if (urlParams.has('bigbutton')){ - session.bigmutebutton=true; +if (urlParams.has('bigbutton')) { + session.bigmutebutton = true; getById("mutebutton").style.width = "min(40vh,40vw)"; getById("mutebutton").style.height = "min(40vh,40vw)"; getById("mutetoggle").style.width = "min(40vh,40vw)"; getById("mutetoggle").style.height = "min(40vh,40vw)"; - + } -if (urlParams.has('scene')){ +if (urlParams.has('scene')) { session.scene = parseInt(urlParams.get('scene')) || 0; session.disableWebAudio = true; - session.audioEffects = false; + session.audioEffects = false; session.audioMeterGuest = false; } -if (urlParams.has('mediasettings')){ - session.forceMediaSettings=true; +if (urlParams.has('mediasettings')) { + session.forceMediaSettings = true; } -if (urlParams.has('transcript') || urlParams.has('transcribe') || urlParams.has('trans')){ +if (urlParams.has('transcript') || urlParams.has('transcribe') || urlParams.has('trans')) { session.transcript = urlParams.get('transcript') || urlParams.get('transcribe') || urlParams.get('trans') || "en-US"; } -if (urlParams.has('cc') || urlParams.has('closedcaptions') || urlParams.has('captions')){ +if (urlParams.has('cc') || urlParams.has('closedcaptions') || urlParams.has('captions')) { session.closedCaptions = true; } -if (session.webcamonly==true){ +if (session.webcamonly == true) { getById("container-2").className = 'column columnfade advanced'; // Hide screen share on mobile getById("container-3").classList.add("skip-animation"); getById("container-3").classList.remove('pointer'); - setTimeout(function(){previewWebcam();},100); + setTimeout(function() { + previewWebcam(); + }, 100); } getById("main").classList.remove('hidden'); -if (urlParams.has('password') || urlParams.has('pass') || urlParams.has('pw') || urlParams.has('p')){ +if (urlParams.has('password') || urlParams.has('pass') || urlParams.has('pw') || urlParams.has('p')) { session.password = urlParams.get('password') || urlParams.get('pass') || urlParams.get('pw') || urlParams.get('p'); - if (!session.password){ + if (!session.password) { session.password = prompt("Please enter the password below: \n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)"); - } else if (session.password==="false"){ - session.password=false; - session.defaultPassword=false; - } else if (session.password==="0"){ - session.password=false; - session.defaultPassword=false; - } else if (session.password==="off"){ - session.password=false; - session.defaultPassword=false; + } else if (session.password === "false") { + session.password = false; + session.defaultPassword = false; + } else if (session.password === "0") { + session.password = false; + session.defaultPassword = false; + } else if (session.password === "off") { + session.password = false; + session.defaultPassword = false; } } -if (session.password){ +if (session.password) { session.password = sanitizePassword(session.password); getById("passwordRoom").value = session.password; session.defaultPassword = false; } -if (urlParams.has('hash') || urlParams.has('crc') || urlParams.has('check')){ // could be brute forced in theory, so not as safe as just not using a hash check. - session.taintedSession=null; // waiting to see if valid or not. +if (urlParams.has('hash') || urlParams.has('crc') || urlParams.has('check')) { // could be brute forced in theory, so not as safe as just not using a hash check. + session.taintedSession = null; // waiting to see if valid or not. var hash_input = urlParams.get('hash') || urlParams.get('crc') || urlParams.get('check'); - if (session.password===false){ + if (session.password === false) { session.password = prompt("Please enter the password below: \n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)"); session.password = sanitizePassword(session.password); getById("passwordRoom").value = session.password; session.defaultPassword = false; - } - - session.generateHash(session.password+session.salt,6).then(function(hash){ // million to one error. - log("hash is "+hash); - if (hash.substring(0, 4) !== hash_input){ // hash crc checks are just first 4 characters. - session.taintedSession=true; - if (!(session.cleanOutput)){ + } + + session.generateHash(session.password + session.salt, 6).then(function(hash) { // million to one error. + log("hash is " + hash); + if (hash.substring(0, 4) !== hash_input) { // hash crc checks are just first 4 characters. + session.taintedSession = true; + if (!(session.cleanOutput)) { getById("request_info_prompt").innerHTML = "The password was incorrect.\n\nRefresh and try again."; getById("request_info_prompt").style.display = "block"; getById("mainmenu").style.display = "none"; getById("head1").style.display = "none"; session.cleanOutput = true; - + } else { getById("request_info_prompt").innerHTML = ""; getById("request_info_prompt").style.display = "block"; @@ -751,20 +895,20 @@ if (urlParams.has('hash') || urlParams.has('crc') || urlParams.has('check')){ / getById("head1").style.display = "none"; } } else { - session.taintedSession=false; + session.taintedSession = false; session.hash = hash; } }); } -if (session.defaultPassword!==false){ +if (session.defaultPassword !== false) { session.password = session.defaultPassword; // no user entered password; let's use the default password if its not disabled. } -if (urlParams.has('showlabels') || urlParams.has('showlabel') || urlParams.has('sl')){ - session.showlabels = urlParams.get('showlabels') || urlParams.get('showlabel') || urlParams.get('sl') || ""; - session.showlabels = sanitizeLabel(session.showlabels.replace(/[\W]+/g,"_").replace(/_+/g, '_')); - if (session.showlabels==""){ +if (urlParams.has('showlabels') || urlParams.has('showlabel') || urlParams.has('sl')) { + session.showlabels = urlParams.get('showlabels') || urlParams.get('showlabel') || urlParams.get('sl') || ""; + session.showlabels = sanitizeLabel(session.showlabels.replace(/[\W]+/g, "_").replace(/_+/g, '_')); + if (session.showlabels == "") { session.showlabels = true; session.labelstyle = false; } else { @@ -773,113 +917,130 @@ if (urlParams.has('showlabels') || urlParams.has('showlabel') || urlParams.has( } } -if (urlParams.has('sizelabel') || urlParams.has('labelsize') || urlParams.has('fontsize')){ - session.labelsize = urlParams.get('sizelabel') || urlParams.get('labelsize') || urlParams.get('fontsize') || 100; +if (urlParams.has('sizelabel') || urlParams.has('labelsize') || urlParams.has('fontsize')) { + session.labelsize = urlParams.get('sizelabel') || urlParams.get('labelsize') || urlParams.get('fontsize') || 100; session.labelsize = parseInt(session.labelsize); } -if (urlParams.has('label') || urlParams.has('l')){ +if (urlParams.has('label') || urlParams.has('l')) { session.label = urlParams.get('label') || urlParams.get('l'); - var updateURLAsNeed=true; - if (session.label == null || session.label.length==0){ + var updateURLAsNeed = true; + if (session.label == null || session.label.length == 0) { session.label = prompt("Please enter your display name:"); } else { - var updateURLAsNeed=false; + var updateURLAsNeed = false; session.label = decodeURIComponent(session.label); } - if (session.label!=null){ - session.label = sanitizeLabel(session.label); // alphanumeric was too strict. - document.title=session.label; // what the result is. - - if (updateURLAsNeed){ + if (session.label != null) { + session.label = sanitizeLabel(session.label); // alphanumeric was too strict. + document.title = session.label; // what the result is. + + if (updateURLAsNeed) { var label = encodeURIComponent(session.label); - if (urlParams.has('l')){ - updateURL("l="+label, true); + if (urlParams.has('l')) { + updateURL("l=" + label, true, false); } else { - updateURL("label="+label, true); + updateURL("label=" + label, true, false); } } } } -if (urlParams.has('transparent')){ // sets the window to be transparent - useful for IFRAMES? +if (urlParams.has('transparent')) { // sets the window to be transparent - useful for IFRAMES? getById("main").style.backgroundColor = "rgba(0,0,0,0)"; } -if (urlParams.has('stereo') || urlParams.has('s') || urlParams.has('proaudio')){ // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono +if (urlParams.has('stereo') || urlParams.has('s') || urlParams.has('proaudio')) { // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono log("STEREO ENABLED"); session.stereo = urlParams.get('stereo') || urlParams.get('s') || urlParams.get('proaudio'); - - if (session.stereo){ + + if (session.stereo) { session.stereo = session.stereo.toLowerCase(); } - - if (session.stereo==="false"){ + + //var supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + //supportedConstraints.channelCount; + + if (session.stereo === "false") { session.stereo = 0; - } else if (session.stereo==="0"){ + session.audioInputChannels = 1; + } else if (session.stereo === "0") { session.stereo = 0; - } else if (session.stereo==="no"){ + session.audioInputChannels = 1; + } else if (session.stereo === "no") { session.stereo = 0; - } else if (session.stereo==="off"){ + session.audioInputChannels = 1; + } else if (session.stereo === "off") { session.stereo = 0; - } else if (session.stereo==="1"){ + session.audioInputChannels = 1; + + } else if (session.stereo === "1") { session.stereo = 1; - } else if (session.stereo==="both"){ + } else if (session.stereo === "both") { session.stereo = 1; - } else if (session.stereo==="3"){ + } else if (session.stereo === "3") { session.stereo = 3; - } else if (session.stereo==="out"){ + } else if (session.stereo === "out") { session.stereo = 3; - } else if (session.stereo==="mono"){ + } else if (session.stereo === "mono") { session.stereo = 3; session.audiobitrate = 128; - } else if (session.stereo==="4"){ + } else if (session.stereo === "4") { session.stereo = 4; - } else if (session.stereo==="multi"){ + } else if (session.stereo === "multi") { session.stereo = 4; - } else if (session.stereo==="2"){ + } else if (session.stereo === "2") { session.stereo = 2; - } else if (session.stereo==="in"){ + } else if (session.stereo === "in") { session.stereo = 2; } else { session.stereo = 5; // guests; no stereo in, no high bitrate in, but otherwise like stereo=1 } } -if ((session.stereo==1) || (session.stereo==3) || (session.stereo==4) || (session.stereo==5)){ +if ((session.stereo == 1) || (session.stereo == 3) || (session.stereo == 4) || (session.stereo == 5)) { session.echoCancellation = false; session.autoGainControl = false; session.noiseSuppression = false; } -if (urlParams.has('mono')){ - session.mono=true; - if ((session.stereo==1) || (session.stereo==4)){ +if (urlParams.has('mono')) { + session.mono = true; + if ((session.stereo == 1) || (session.stereo == 4)) { session.stereo = 3; session.audiobitrate = 128; - } else if (session.stereo==5){ + } else if (session.stereo == 5) { session.stereo = 3; session.audiobitrate = 128; - } else if (session.stereo==2){ + } else if (session.stereo == 2) { session.stereo = 0; session.audiobitrate = 128; } } -if (urlParams.has("aec") || urlParams.has("ec")){ - +if (urlParams.has("channelcount") || urlParams.has("ac")) { + session.audioInputChannels = urlParams.get('channelcount') || urlParams.get('ac'); + session.audioInputChannels == parseInt(session.audioInputChannels); + if (!session.audioInputChannels) { + session.audioInputChannels = false; + } +} + + +if (urlParams.has("aec") || urlParams.has("ec")) { + session.echoCancellation = urlParams.get('aec') || urlParams.get('ec'); - - if (session.echoCancellation){ + + if (session.echoCancellation) { session.echoCancellation = session.echoCancellation.toLowerCase(); } - if (session.echoCancellation=="false"){ + if (session.echoCancellation == "false") { session.echoCancellation = false; - } else if (session.echoCancellation=="0"){ + } else if (session.echoCancellation == "0") { session.echoCancellation = false; - } else if (session.echoCancellation=="no"){ + } else if (session.echoCancellation == "no") { session.echoCancellation = false; - } else if (session.echoCancellation=="off"){ + } else if (session.echoCancellation == "off") { session.echoCancellation = false; } else { session.echoCancellation = true; @@ -887,39 +1048,39 @@ if (urlParams.has("aec") || urlParams.has("ec")){ } -if (urlParams.has("autogain") || urlParams.has("ag")){ - +if (urlParams.has("autogain") || urlParams.has("ag")) { + session.autoGainControl = urlParams.get('autogain') || urlParams.get('ag'); - if (session.autoGainControl){ + if (session.autoGainControl) { session.autoGainControl = session.autoGainControl.toLowerCase(); } - if (session.autoGainControl=="false"){ + if (session.autoGainControl == "false") { session.autoGainControl = false; - } else if (session.autoGainControl=="0"){ + } else if (session.autoGainControl == "0") { session.autoGainControl = false; - } else if (session.autoGainControl=="no"){ + } else if (session.autoGainControl == "no") { session.autoGainControl = false; - } else if (session.autoGainControl=="off"){ + } else if (session.autoGainControl == "off") { session.autoGainControl = false; } else { session.autoGainControl = true; } } -if (urlParams.has("denoise") || urlParams.has("dn")){ - +if (urlParams.has("denoise") || urlParams.has("dn")) { + session.noiseSuppression = urlParams.get('denoise') || urlParams.get('dn'); - - if (session.noiseSuppression){ + + if (session.noiseSuppression) { session.noiseSuppression = session.noiseSuppression.toLowerCase(); } - if (session.noiseSuppression=="false"){ + if (session.noiseSuppression == "false") { session.noiseSuppression = false; - } else if (session.noiseSuppression=="0"){ + } else if (session.noiseSuppression == "0") { session.noiseSuppression = false; - } else if (session.noiseSuppression=="no"){ + } else if (session.noiseSuppression == "no") { session.noiseSuppression = false; - } else if (session.noiseSuppression=="off"){ + } else if (session.noiseSuppression == "off") { session.noiseSuppression = false; } else { session.noiseSuppression = true; @@ -927,27 +1088,27 @@ if (urlParams.has("denoise") || urlParams.has("dn")){ } -if (urlParams.has('roombitrate') || urlParams.has('roomvideobitrate') || urlParams.has('rbr')){ +if (urlParams.has('roombitrate') || urlParams.has('roomvideobitrate') || urlParams.has('rbr')) { log("Room BITRATE SET"); session.roombitrate = urlParams.get('roombitrate') || urlParams.get('rbr') || urlParams.get('roomvideobitrate'); session.roombitrate = parseInt(session.roombitrate); - if (session.roombitrate<1){ - session.roombitrate=0; + if (session.roombitrate < 1) { + session.roombitrate = 0; } } -if (urlParams.has('audiobitrate') || urlParams.has('ab')){ // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono +if (urlParams.has('audiobitrate') || urlParams.has('ab')) { // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono log("AUDIO BITRATE SET"); session.audiobitrate = urlParams.get('audiobitrate') || urlParams.get('ab'); session.audiobitrate = parseInt(session.audiobitrate); - if (session.audiobitrate<1){ - session.audiobitrate=false; - } else if (session.audiobitrate>510){ - session.audiobitrate=510; + if (session.audiobitrate < 1) { + session.audiobitrate = false; + } else if (session.audiobitrate > 510) { + session.audiobitrate = 510; } // this is to just prevent abuse } -if ((iOS) || (iPad)){ +if ((iOS) || (iPad)) { session.audiobitrate = false; // iOS devices seem to get distortion with custom audio bitrates. Disable for now. } @@ -958,221 +1119,225 @@ if ((iOS) || (iPad)){ } catch (e){errorlog(e);} } */ -if (urlParams.has('streamid') || urlParams.has('view') || urlParams.has('v') || urlParams.has('pull')){ // the streams we want to view; if set, but let blank, we will request no streams to watch. +if (urlParams.has('streamid') || urlParams.has('view') || urlParams.has('v') || urlParams.has('pull')) { // the streams we want to view; if set, but let blank, we will request no streams to watch. session.view = urlParams.get('streamid') || urlParams.get('view') || urlParams.get('v') || urlParams.get('pull'); // this value can be comma seperated for multiple streams to pull - - getById("headphonesDiv2").style.display="inline-block"; - getById("headphonesDiv").style.display="inline-block"; - - if (session.view==null){ - session.view=""; + + getById("headphonesDiv2").style.display = "inline-block"; + getById("headphonesDiv").style.display = "inline-block"; + + if (session.view == null) { + session.view = ""; } - if (session.view){ - if (session.view.split(",").length>1){ + if (session.view) { + if (session.view.split(",").length > 1) { session.view_set = session.view.split(","); - } + } } } -if (urlParams.has('nopreview') || urlParams.has('np')){ +if (urlParams.has('nopreview') || urlParams.has('np')) { log("preview OFF"); - session.nopreview = true; -} else if ((urlParams.has('preview')) || (urlParams.has('showpreview'))){ + session.nopreview = true; +} else if ((urlParams.has('preview')) || (urlParams.has('showpreview'))) { log("preview ON"); - session.nopreview = false; -} + session.nopreview = false; +} -if (urlParams.has('obsfix')){ +if (urlParams.has('obsfix')) { session.obsfix = urlParams.get('obsfix'); - if (session.obsfix){ + if (session.obsfix) { session.obsfix = session.obsfix.toLowerCase(); } - if (session.obsfix=="false"){ + if (session.obsfix == "false") { session.obsfix = false; - } else if (session.obsfix=="0"){ + } else if (session.obsfix == "0") { session.obsfix = false; - } else if (session.obsfix=="no"){ + } else if (session.obsfix == "no") { session.obsfix = false; - } else if (session.obsfix=="off"){ + } else if (session.obsfix == "off") { session.obsfix = false; - } else if (parseInt(session.obsfix)>0){ + } else if (parseInt(session.obsfix) > 0) { session.obsfix = parseInt(session.obsfix); } else { session.obsfix = 1; // aggressive. } } -if (urlParams.has('controlroombitrate') || urlParams.has('crb')){ - session.controlRoomBitrate=true; +if (urlParams.has('controlroombitrate') || urlParams.has('crb')) { + session.controlRoomBitrate = true; } -if (urlParams.has('remote') || urlParams.has('rem')){ +if (urlParams.has('remote') || urlParams.has('rem')) { log("remote ENABLED"); session.remote = urlParams.get('remote') || urlParams.get('rem'); - session.remote = session.remote.trim(); + session.remote = session.remote.trim(); } -if (urlParams.has('latency') || urlParams.has('al') || urlParams.has('audiolatency')){ +if (urlParams.has('latency') || urlParams.has('al') || urlParams.has('audiolatency')) { log("latency ENABLED"); session.audioLatency = urlParams.get('latency') || urlParams.get('al') || urlParams.get('audiolatency'); - session.audioLatency = parseInt(session.audioLatency) || 0 ; - session.disableWebAudio=false; + session.audioLatency = parseInt(session.audioLatency) || 0; + session.disableWebAudio = false; } -if (urlParams.has('audiogain') || urlParams.has('gain') || urlParams.has('g')){ +if (urlParams.has('audiogain') || urlParams.has('gain') || urlParams.has('g')) { log("audio gain ENABLED"); session.audioGain = urlParams.get('audiogain') || urlParams.get('gain') || urlParams.get('g'); - session.audioGain = parseInt(session.audioGain) || 0 ; - session.disableWebAudio=false; + session.audioGain = parseInt(session.audioGain) || 0; + session.disableWebAudio = false; } -if (urlParams.has('compressor') || urlParams.has('comp')){ +if (urlParams.has('compressor') || urlParams.has('comp')) { log("audio gain ENABLED"); session.compressor = 1; - session.disableWebAudio=false; -} else if (urlParams.has('limiter')){ + session.disableWebAudio = false; +} else if (urlParams.has('limiter')) { log("audio gain ENABLED"); session.compressor = 2; - session.disableWebAudio=false; -} -if ((urlParams.has('equalizer')) || (urlParams.has('eq'))){ + session.disableWebAudio = false; +} +if ((urlParams.has('equalizer')) || (urlParams.has('eq'))) { session.equalizer = true; - session.disableWebAudio=false; + session.disableWebAudio = false; +} +if ((urlParams.has('lowcut')) || (urlParams.has('lc')) || (urlParams.has('higpass'))) { + session.lowcut = urlParams.get('lowcut') || urlParams.get('lc') || urlParams.get('higpass') || 100; + session.lowcut = parseInt(session.lowcut); + session.disableWebAudio = false; } -if (urlParams.has('keyframeinterval') || urlParams.has('keyframerate') || urlParams.has('keyframe') ||urlParams.has('fki')){ +if (urlParams.has('keyframeinterval') || urlParams.has('keyframerate') || urlParams.has('keyframe') || urlParams.has('fki')) { log("keyframerate ENABLED"); session.keyframerate = parseInt(urlParams.get('keyframeinterval') || urlParams.get('keyframerate') || urlParams.get('keyframe') || urlParams.get('fki')) || 0; } -if (urlParams.has('optimize')){ - var optimize = parseInt(urlParams.get('optimize')); - if (optimize!==0){ - optimize = optimize || 600; - } - session.optimize = optimize; -} - -if (urlParams.has('tallyoff') || urlParams.has('obsoff') || urlParams.has('oo')){ +if (urlParams.has('tallyoff') || urlParams.has('obsoff') || urlParams.has('oo')) { log("OBS feedback disabled"); - session.disableOBS = true; + session.disableOBS = true; } -if (urlParams.has('chroma')){ +if (urlParams.has('chroma')) { log("Chroma ENABLED"); - getById("main").style.backgroundColor = "#"+(urlParams.get('chroma') || "000"); + getById("main").style.backgroundColor = "#" + (urlParams.get('chroma') || "000"); } -if (urlParams.has("videodevice") || urlParams.has("vdevice") || urlParams.has("vd") || urlParams.has("device") || urlParams.has("d")){ - - session.videoDevice = urlParams.get("videodevice") || urlParams.get("vdevice") || urlParams.get("vd") || urlParams.get("device") || urlParams.get("d"); - - if (session.videoDevice===null){ +if (urlParams.has("videodevice") || urlParams.has("vdevice") || urlParams.has("vd") || urlParams.has("device") || urlParams.has("d")) { + + session.videoDevice = urlParams.get("videodevice") || urlParams.get("vdevice") || urlParams.get("vd") || urlParams.get("device") || urlParams.get("d"); + + if (session.videoDevice === null) { session.videoDevice = "1"; - } else if (session.videoDevice){ - session.videoDevice = session.videoDevice.toLowerCase().replace(/[\W]+/g,"_"); + } else if (session.videoDevice) { + session.videoDevice = session.videoDevice.toLowerCase().replace(/[\W]+/g, "_"); } - if (session.videoDevice=="false"){ + if (session.videoDevice == "false") { session.videoDevice = 0; - } else if (session.videoDevice=="0"){ + } else if (session.videoDevice == "0") { session.videoDevice = 0; - } else if (session.videoDevice=="no"){ + } else if (session.videoDevice == "no") { session.videoDevice = 0; - } else if (session.videoDevice=="off"){ + } else if (session.videoDevice == "off") { session.videoDevice = 0; - } else if (session.videoDevice=="snapcam"){ + } else if (session.videoDevice == "snapcam") { session.videoDevice = "snap_camera"; - } else if (session.videoDevice=="canon"){ + } else if (session.videoDevice == "canon") { session.videoDevice = "eos"; - } else if (session.videoDevice=="camlink"){ + } else if (session.videoDevice == "camlink") { session.videoDevice = "cam_link"; - } else if (session.videoDevice=="ndi"){ + } else if (session.videoDevice == "ndi") { session.videoDevice = "newtek_ndi_video"; - } else if (session.videoDevice==""){ + } else if (session.videoDevice == "") { session.videoDevice = 1; - } else if (session.videoDevice=="1"){ + } else if (session.videoDevice == "1") { session.videoDevice = 1; - } else if (session.videoDevice=="default"){ + } else if (session.videoDevice == "default") { session.videoDevice = 1; } else { // whatever the user entered I guess, santitized. - session.videoDevice = session.videoDevice.replace(/[\W]+/g,"_").toLowerCase(); + session.videoDevice = session.videoDevice.replace(/[\W]+/g, "_").toLowerCase(); } - - if (session.videoDevice === 0){ + + if (session.videoDevice === 0) { getById("add_camera").innerHTML = "Share your Microphone"; - miniTranslate(getById("add_camera"),"share-your-mic"); + miniTranslate(getById("add_camera"), "share-your-mic"); } - - getById("videoMenu").style.display="none"; - log("session.videoDevice:"+session.videoDevice); + + getById("videoMenu").style.display = "none"; + log("session.videoDevice:" + session.videoDevice); } // audioDevice -if (urlParams.has("audiodevice") || urlParams.has("adevice") || urlParams.has("ad") || urlParams.has("device") || urlParams.has("d")){ - - session.audioDevice = urlParams.get("audiodevice") || urlParams.get("adevice") || urlParams.get("ad") || urlParams.get("device") || urlParams.get("d"); - - if (session.audioDevice===null){ - session.audioDevice="1"; - } else if (session.audioDevice){ - session.audioDevice = session.audioDevice.toLowerCase().replace(/[\W]+/g,"_"); +if (urlParams.has("audiodevice") || urlParams.has("adevice") || urlParams.has("ad") || urlParams.has("device") || urlParams.has("d")) { + + session.audioDevice = urlParams.get("audiodevice") || urlParams.get("adevice") || urlParams.get("ad") || urlParams.get("device") || urlParams.get("d"); + + if (session.audioDevice === null) { + session.audioDevice = "1"; + } else if (session.audioDevice) { + session.audioDevice = session.audioDevice.toLowerCase().replace(/[\W]+/g, "_"); } - - if (session.audioDevice=="false"){ + + if (session.audioDevice == "false") { session.audioDevice = 0; - } else if (session.audioDevice=="0"){ + } else if (session.audioDevice == "0") { session.audioDevice = 0; - } else if (session.audioDevice=="no"){ + } else if (session.audioDevice == "no") { session.audioDevice = 0; - } else if (session.audioDevice=="off"){ + } else if (session.audioDevice == "off") { session.audioDevice = 0; - } else if (session.audioDevice==""){ + } else if (session.audioDevice == "") { session.audioDevice = 1; - } else if (session.audioDevice=="1"){ + } else if (session.audioDevice == "1") { session.audioDevice = 1; - } else if (session.audioDevice=="default"){ + } else if (session.audioDevice == "default") { session.audioDevice = 1; - } else if (session.audioDevice=="ndi"){ - session.audioDevice="line_newtek_ndi_audio"; + } else if (session.audioDevice == "ndi") { + session.audioDevice = "line_newtek_ndi_audio"; } else { // whatever the user entered I guess - session.audioDevice = session.audioDevice.replace(/[\W]+/g,"_").toLowerCase(); + session.audioDevice = session.audioDevice.replace(/[\W]+/g, "_").toLowerCase(); } - - - if (session.videoDevice === 0){ - if (session.audioDevice === 0){ + + + if (session.videoDevice === 0) { + if (session.audioDevice === 0) { getById("add_camera").innerHTML = "Click Start to Join"; - miniTranslate(getById("add_camera"),"click-start-to-join"); + miniTranslate(getById("add_camera"), "click-start-to-join"); getById("container-2").className = 'column columnfade advanced'; // Hide screen share on mobile getById("container-3").classList.add("skip-animation"); getById("container-3").classList.remove('pointer'); - setTimeout(function(){previewWebcam();},100); + setTimeout(function() { + previewWebcam(); + }, 100); session.webcamonly = true; } } - + log("session.audioDevice:" + session.audioDevice); - - getById("audioMenu").style.display="none"; - getById("headphonesDiv").style.display="none"; - getById("headphonesDiv2").style.display="none"; - getById("audioScreenShare1").style.display="none"; - + + getById("audioMenu").style.display = "none"; + getById("headphonesDiv").style.display = "none"; + getById("headphonesDiv2").style.display = "none"; + getById("audioScreenShare1").style.display = "none"; + } -if (urlParams.has("autojoin") || urlParams.has("autostart") || urlParams.has('aj') || urlParams.has('as')){ +if (urlParams.has("autojoin") || urlParams.has("autostart") || urlParams.has('aj') || urlParams.has('as')) { session.autostart = true; + if (session.screenshare) { + setTimeout(function() { + publishScreen(); + }, 200); + } } -if (urlParams.has('noiframe') || urlParams.has('noiframes') || urlParams.has('nif')){ - +if (urlParams.has('noiframe') || urlParams.has('noiframes') || urlParams.has('nif')) { + session.noiframe = urlParams.get('noiframe') || urlParams.get('noiframes') || urlParams.get('nif'); - - if (!(session.noiframe)){ - session.noiframe=[]; + + if (!(session.noiframe)) { + session.noiframe = []; } else { session.noiframe = session.noiframe.split(","); } @@ -1181,11 +1346,11 @@ if (urlParams.has('noiframe') || urlParams.has('noiframes') || urlParams.has('ni } -if (urlParams.has('exclude') || urlParams.has('ex')){ - +if (urlParams.has('exclude') || urlParams.has('ex')) { + session.exclude = urlParams.get('exclude') || urlParams.get('ex'); - - if (!(session.exclude)){ + + if (!(session.exclude)) { session.exclude = false; } else { session.exclude = session.exclude.split(","); @@ -1195,12 +1360,12 @@ if (urlParams.has('exclude') || urlParams.has('ex')){ } -if (urlParams.has('novideo') || urlParams.has('nv') || urlParams.has('hidevideo') || urlParams.has('showonly') ){ - +if (urlParams.has('novideo') || urlParams.has('nv') || urlParams.has('hidevideo') || urlParams.has('showonly')) { + session.novideo = urlParams.get('novideo') || urlParams.get('nv') || urlParams.get('hidevideo') || urlParams.get('showonly'); - - if (!(session.novideo)){ - session.novideo=[]; + + if (!(session.novideo)) { + session.novideo = []; } else { session.novideo = session.novideo.split(","); } @@ -1208,24 +1373,24 @@ if (urlParams.has('novideo') || urlParams.has('nv') || urlParams.has('hidevideo' log(session.novideo); } -if (urlParams.has('noaudio') || urlParams.has('na') || urlParams.has('hideaudio') ){ - - session.noaudio = urlParams.get('noaudio') || urlParams.get('na') || urlParams.get('hideaudio') ; - - if (!(session.noaudio)){ - session.noaudio=[]; +if (urlParams.has('noaudio') || urlParams.has('na') || urlParams.has('hideaudio')) { + + session.noaudio = urlParams.get('noaudio') || urlParams.get('na') || urlParams.get('hideaudio'); + + if (!(session.noaudio)) { + session.noaudio = []; } else { session.noaudio = session.noaudio.split(","); } log("disable audio playback"); } -if (urlParams.has('forceios')){ +if (urlParams.has('forceios')) { log("allow iOS to work in video group chat; for this user at least"); - session.forceios = true; + session.forceios = true; } -if (urlParams.has('nocursor')){ +if (urlParams.has('nocursor')) { session.nocursor = true; log("DISABLE CURSOR"); var style = document.createElement('style'); @@ -1241,76 +1406,94 @@ if (urlParams.has('nocursor')){ document.head.appendChild(style); } -if (urlParams.has('vbr')){ +if (urlParams.has('vbr')) { session.cbr = 0; -} - -if (urlParams.has('order')){ +} + +if (urlParams.has('order')) { session.order = parseInt(urlParams.get('order')) || 0; -} -if (urlParams.has('sensors') || urlParams.has('sensor') || urlParams.has('gyro') || urlParams.has('gyros') || urlParams.has('accelerometer')){ +} +if (urlParams.has('sensors') || urlParams.has('sensor') || urlParams.has('gyro') || urlParams.has('gyros') || urlParams.has('accelerometer')) { session.sensorData = urlParams.get('sensors') || urlParams.get('sensor') || urlParams.get('gyro') || urlParams.get('gyros') || urlParams.get('accelerometer') || 30; session.sensorData = parseInt(session.sensorData); } -if (urlParams.has('minptime')){ - session.minptime = parseInt(urlParams.get('minptime')) || 10; - if (session.minptime<10){session.minptime=10;} - if (session.minptime>300){session.minptime=300;} +if (urlParams.has('minptime')) { + session.minptime = parseInt(urlParams.get('minptime')) || 10; + if (session.minptime < 10) { + session.minptime = 10; + } + if (session.minptime > 300) { + session.minptime = 300; + } } -if (urlParams.has('maxptime')){ +if (urlParams.has('maxptime')) { session.maxptime = parseInt(urlParams.get('maxptime')) || 60; - if (session.maxptime<10){session.maxptime=10;} - if (session.maxptime>300){session.maxptime=300;} + if (session.maxptime < 10) { + session.maxptime = 10; + } + if (session.maxptime > 300) { + session.maxptime = 300; + } } -if (urlParams.has('ptime')){ - session.ptime = parseInt(urlParams.get('ptime')) || 20; - if (session.minptime<10){session.ptime=10;} - if (session.minptime>300){session.ptime=300;} +if (urlParams.has('ptime')) { + session.ptime = parseInt(urlParams.get('ptime')) || 20; + if (session.minptime < 10) { + session.ptime = 10; + } + if (session.minptime > 300) { + session.ptime = 300; + } } -if (urlParams.has('codec')){ +if (urlParams.has('codec')) { log("CODEC CHANGED"); - session.codec = urlParams.get('codec').toLowerCase(); + session.codec = urlParams.get('codec').toLowerCase(); } //else if (window.obsstudio){ - //if (session.obsfix===false){ - // session.codec = "h264"; // H264 --- It's too laggy!!! FUCKEEEEEEE - //} +//if (session.obsfix===false){ +// session.codec = "h264"; // H264 --- It's too laggy!!! FUCKEEEEEEE +//} //} -if (urlParams.has('scale')){ - log("Resolution scale requested"); - session.scale = urlParams.get('scale'); + +if (urlParams.has('scale')) { + if (urlParams.get('scale') == "false") {} else if (urlParams.get('scale') == "0") {} else if (urlParams.get('scale') == "no") {} else if (urlParams.get('scale') == "off") {} else { + log("Resolution scale requested"); + session.scale = parseInt(urlParams.get('scale')) || 100; + } + session.dynamicScale = false; // default true } var ConfigSettings = getById("main-js"); var ln_template = false; var translation = false; try { - if (ConfigSettings){ - ln_template = ConfigSettings.getAttribute('data-translation'); // Translations - if (typeof ln_template === "undefined" ) { - ln_template = false; - } else if (ln_template === null ) { - ln_template = false; + if (ConfigSettings) { + ln_template = ConfigSettings.getAttribute('data-translation'); // Translations + if (typeof ln_template === "undefined") { + ln_template = false; + } else if (ln_template === null) { + ln_template = false; } } - if (urlParams.has('ln')){ + if (urlParams.has('ln')) { ln_template = urlParams.get('ln'); - } -} catch (e){errorlog(e);} + } +} catch (e) { + errorlog(e); +} -if (ln_template){ // checking if manual lanuage override enabled +if (ln_template) { // checking if manual lanuage override enabled try { - log("Template: "+ln_template); - fetch("./translations/"+ln_template+'.json').then(function(response){ + log("Template: " + ln_template); + fetch("./translations/" + ln_template + '.json').then(function(response) { if (response.status !== 200) { log('Looks like there was a problem. Status Code: ' + - response.status); + response.status); return; } response.json().then(function(data) { @@ -1318,47 +1501,47 @@ if (ln_template){ // checking if manual lanuage override enabled translation = data; var trans = data.innerHTML; var allItems = document.querySelectorAll('[data-translate]'); - allItems.forEach(function(ele){ - if (ele.dataset.translate in trans){ + allItems.forEach(function(ele) { + if (ele.dataset.translate in trans) { ele.innerHTML = trans[ele.dataset.translate]; } }); trans = data.titles; var allTitles = document.querySelectorAll('[title]'); - allTitles.forEach(function(ele){ - var key = ele.title.replace(/[\W]+/g,"-").toLowerCase(); - if (key in trans){ + allTitles.forEach(function(ele) { + var key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); + if (key in trans) { ele.title = trans[key]; } }); trans = data.placeholders; var allPlaceholders = document.querySelectorAll('[placeholder]'); - allPlaceholders.forEach(function(ele){ - var key = ele.placeholder.replace(/[\W]+/g,"-").toLowerCase(); - if (key in trans){ + allPlaceholders.forEach(function(ele) { + var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); + if (key in trans) { ele.placeholder = trans[key]; } }); - - + + getById("mainmenu").style.opacity = 1; - }).catch(function(err){ + }).catch(function(err) { errorlog(err); getById("mainmenu").style.opacity = 1; }); - }).catch(function(err){ + }).catch(function(err) { errorlog(err); getById("mainmenu").style.opacity = 1; }); - - } catch (error){ + + } catch (error) { errorlog(error); getById("mainmenu").style.opacity = 1; } -} else if (location.hostname !== "obs.ninja"){ - if (location.hostname === "rtc.ninja"){ - try{ - if (session.label===false){ +} else if (location.hostname !== "obs.ninja") { + if (location.hostname === "rtc.ninja") { + try { + if (session.label === false) { document.title = ""; } getById("qos").innerHTML = ""; @@ -1366,6 +1549,7 @@ if (ln_template){ // checking if manual lanuage override enabled getById("helpbutton").style.display = "none"; getById("helpbutton").style.opacity = 0; getById("reportbutton").style.display = "none"; + getById("reportbutton").style.opacity = 0; getById("mainmenu").style.opacity = 1; getById("mainmenu").style.margin = "30px 0"; getById("translateButton").style.display = "none"; @@ -1373,60 +1557,59 @@ if (ln_template){ // checking if manual lanuage override enabled getById("info").style.display = "none"; getById("info").style.opacity = 0; getById("chatBody").innerHTML = ""; - - } catch(e){} + } catch (e) {} } try { - fetch("./translations/blank.json").then(function(response){ + fetch("./translations/blank.json").then(function(response) { if (response.status !== 200) { log('Looks like there was a problem. Status Code: ' + - response.status); + response.status); return; } response.json().then(function(data) { log(data); - + var trans = data.innerHTML; var allItems = document.querySelectorAll('[data-translate]'); - allItems.forEach(function(ele){ - if (ele.dataset.translate in trans){ + allItems.forEach(function(ele) { + if (ele.dataset.translate in trans) { ele.innerHTML = trans[ele.dataset.translate]; } }); trans = data.titles; var allTitles = document.querySelectorAll('[title]'); - allTitles.forEach(function(ele){ - var key = ele.title.replace(/[\W]+/g,"-").toLowerCase(); - if (key in trans){ + allTitles.forEach(function(ele) { + var key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); + if (key in trans) { ele.title = trans[key]; } }); trans = data.placeholders; var allPlaceholders = document.querySelectorAll('[placeholder]'); - allPlaceholders.forEach(function(ele){ - var key = ele.placeholder.replace(/[\W]+/g,"-").toLowerCase(); - if (key in trans){ + allPlaceholders.forEach(function(ele) { + var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); + if (key in trans) { ele.placeholder = trans[key]; } }); - - if (session.label===false){ + + if (session.label === false) { document.title = location.hostname; } getById("qos").innerHTML = location.hostname; - getById("logoname").innerHTML = getById("qos").outerHTML ; + getById("logoname").innerHTML = getById("qos").outerHTML; getById("helpbutton").style.display = "none"; getById("reportbutton").style.display = "none"; getById("mainmenu").style.opacity = 1; - }).catch(function(err){ + }).catch(function(err) { errorlog(err); getById("mainmenu").style.opacity = 1; }); - }).catch(function(err){ + }).catch(function(err) { errorlog(err); getById("mainmenu").style.opacity = 1; }); - if (session.label===false){ + if (session.label === false) { document.title = location.hostname; } getById("qos").innerHTML = location.hostname; @@ -1434,16 +1617,16 @@ if (ln_template){ // checking if manual lanuage override enabled getById("helpbutton").style.display = "none"; getById("reportbutton").style.display = "none"; getById("chatBody").innerHTML = ""; - } catch (error){ + } catch (error) { errorlog(error); } -} else { // check if automatic language translation is available +} else { // check if automatic language translation is available getById("mainmenu").style.opacity = 1; } -try{ - if (location.hostname === "rtc.ninja"){ // an extra-brand-free version of OBS.Ninja - if (session.label===false){ +try { + if (location.hostname === "rtc.ninja") { // an extra-brand-free version of OBS.Ninja + if (session.label === false) { document.title = ""; } getById("qos").innerHTML = ""; @@ -1451,6 +1634,7 @@ try{ getById("helpbutton").style.display = "none"; getById("helpbutton").style.opacity = 0; getById("reportbutton").style.display = "none"; + getById("reportbutton").style.opacity = 0; getById("mainmenu").style.opacity = 1; getById("mainmenu").style.margin = "30px 0"; getById("translateButton").style.display = "none"; @@ -1458,8 +1642,8 @@ try{ getById("info").style.display = "none"; getById("info").style.opacity = 0; getById("chatBody").innerHTML = ""; - } else if (location.hostname !== "obs.ninja"){ - if (session.label===false){ + } else if (location.hostname !== "obs.ninja") { + if (session.label === false) { document.title = location.hostname; } getById("qos").innerHTML = sanitizeLabel(location.hostname); @@ -1467,161 +1651,186 @@ try{ getById("helpbutton").style.display = "none"; getById("reportbutton").style.display = "none"; } - -} catch(e){} -function miniTranslate(ele, ident=false){ - if (ident){ +} catch (e) {} + +if (isIFrame) { + getById("helpbutton").style.display = "none"; + getById("helpbutton").style.opacity = 0; + getById("reportbutton").style.display = "none"; + getById("reportbutton").style.opacity = 0; + getById("chatBody").innerHTML = ""; +} + +function miniTranslate(ele, ident = false) { + if (ident) { ele.dataset.translate = ident; } else { ident = ele.dataset.translate; } try { - if (ident in translation.innerHTML){ + if (ident in translation.innerHTML) { ele.innerHTML = translation.innerHTML[ident]; } - } catch(e){} -} -function changeLg(lang){ - fetch("./translations/"+lang+'.json').then(function(response){ - if (response.status !== 200) { - logerror('Language translation file not found.' + response.status); - return; - } - response.json().then(function(data) { - log(data); - translation = data; // translation.innerHTML[ele.dataset.translate] - var trans = data.innerHTML; - var allItems = document.querySelectorAll('[data-translate]'); - allItems.forEach(function(ele){ - if (ele.dataset.translate in trans){ - ele.innerHTML = trans[ele.dataset.translate]; - } - }); - trans = data.titles; - var allTitles = document.querySelectorAll('[title]'); - allTitles.forEach(function(ele){ - var key = ele.title.replace(/[\W]+/g,"-").toLowerCase(); - if (key in trans){ - ele.title = trans[key]; - } - }); - trans = data.placeholders; - var allPlaceholders = document.querySelectorAll('[placeholder]'); - allPlaceholders.forEach(function(ele){ - var key = ele.placeholder.replace(/[\W]+/g,"-").toLowerCase(); - if (key in trans){ - ele.placeholder = trans[key]; - } - }); - }); - }).catch(function(err){ - errorlog(err); - }); + } catch (e) {} } -if (urlParams.has('videobitrate') || urlParams.has('bitrate') || urlParams.has('vb')){ +function changeLg(lang) { + fetch("./translations/" + lang + '.json').then(function(response) { + if (response.status !== 200) { + logerror('Language translation file not found.' + response.status); + return; + } + response.json().then(function(data) { + log(data); + translation = data; // translation.innerHTML[ele.dataset.translate] + var trans = data.innerHTML; + var allItems = document.querySelectorAll('[data-translate]'); + allItems.forEach(function(ele) { + if (ele.dataset.translate in trans) { + ele.innerHTML = trans[ele.dataset.translate]; + } + }); + trans = data.titles; + var allTitles = document.querySelectorAll('[title]'); + allTitles.forEach(function(ele) { + var key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); + if (key in trans) { + ele.title = trans[key]; + } + }); + trans = data.placeholders; + var allPlaceholders = document.querySelectorAll('[placeholder]'); + allPlaceholders.forEach(function(ele) { + var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); + if (key in trans) { + ele.placeholder = trans[key]; + } + }); + }); + }).catch(function(err) { + errorlog(err); + }); +} + +if (urlParams.has('beep') || urlParams.has('notify') || urlParams.has('tone')) { + session.beepToNotify = true; +} + +if (urlParams.has('r2d2')) { + getById("testtone").innerHTML = ""; + getById("testtone").src = "./media/robot.mp3"; + session.beepToNotify = true; +} + +if (urlParams.has('videobitrate') || urlParams.has('bitrate') || urlParams.has('vb')) { session.bitrate = urlParams.get('videobitrate') || urlParams.get('bitrate') || urlParams.get('vb'); - if (session.bitrate){ - if ((session.view_set) && (session.bitrate.split(",").length>1)){ + if (session.bitrate) { + if ((session.view_set) && (session.bitrate.split(",").length > 1)) { session.bitrate_set = session.bitrate.split(","); session.bitrate = parseInt(session.bitrate_set[0]); } else { session.bitrate = parseInt(session.bitrate); } - if (session.bitrate<1){session.bitrate=false;} + if (session.bitrate < 1) { + session.bitrate = false; + } log("BITRATE ENABLED"); log(session.bitrate); - + } } -if (urlParams.has('maxvideobitrate') || urlParams.has('maxbitrate') || urlParams.has('mvb')){ +if (urlParams.has('maxvideobitrate') || urlParams.has('maxbitrate') || urlParams.has('mvb')) { session.maxvideobitrate = urlParams.get('maxvideobitrate') || urlParams.get('maxbitrate') || urlParams.get('mvb'); - session.maxvideobitrate = parseInt(session.maxvideobitrate); - - if (session.maxvideobitrate<1){session.maxvideobitrate=false;} + session.maxvideobitrate = parseInt(session.maxvideobitrate); + + if (session.maxvideobitrate < 1) { + session.maxvideobitrate = false; + } log("maxvideobitrate ENABLED"); log(session.maxvideobitrate); -} +} -if (urlParams.has('totalroombitrate') || urlParams.has('totalroomvideobitrate')|| urlParams.has('trb')){ +if (urlParams.has('totalroombitrate') || urlParams.has('totalroomvideobitrate') || urlParams.has('trb')) { session.totalRoomBitrate = urlParams.get('totalroombitrate') || urlParams.get('totalroomvideobitrate') || urlParams.get('trb'); - session.totalRoomBitrate = parseInt(session.totalRoomBitrate); - - if (session.totalRoomBitrate<1){session.totalRoomBitrate=false;} + session.totalRoomBitrate = parseInt(session.totalRoomBitrate); + + if (session.totalRoomBitrate < 1) { + session.totalRoomBitrate = false; + } log("totalRoomBitrate ENABLED"); log(session.totalRoomBitrate); -} +} -if (urlParams.has('height') || urlParams.has('h')){ +if (urlParams.has('height') || urlParams.has('h')) { session.height = urlParams.get('height') || urlParams.get('h'); session.height = parseInt(session.height); } -if (urlParams.has('width') || urlParams.has('w')){ +if (urlParams.has('width') || urlParams.has('w')) { session.width = urlParams.get('width') || urlParams.get('w'); session.width = parseInt(session.width); } -if (urlParams.has('quality') || urlParams.has('q')){ - try{ +if (urlParams.has('quality') || urlParams.has('q')) { + try { session.quality = urlParams.get('quality') || urlParams.get('q') || 0; session.quality = parseInt(session.quality); getById("gear_screen").parentNode.removeChild(getById("gear_screen")); getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); - } catch(e){ + } catch (e) { errorlog(e); } } -if (urlParams.has('sink')){ +if (urlParams.has('sink')) { session.sink = urlParams.get('sink'); -} else if (urlParams.has('outputdevice') || urlParams.has('od') || urlParams.has('audiooutput')){ +} else if (urlParams.has('outputdevice') || urlParams.has('od') || urlParams.has('audiooutput')) { session.outputDevice = urlParams.get('outputdevice') || urlParams.get('od') || urlParams.get('audiooutput'); - if (session.outputDevice){ - session.outputDevice = session.outputDevice.toLowerCase().replace(/[\W]+/g,"_"); + if (session.outputDevice) { + session.outputDevice = session.outputDevice.toLowerCase().replace(/[\W]+/g, "_"); } else { session.outputDevice = false; } - if (session.outputDevice){ + if (session.outputDevice) { try { - enumerateDevices().then(function(deviceInfos){ + enumerateDevices().then(function(deviceInfos) { for (let i = 0; i !== deviceInfos.length; ++i) { - if (deviceInfos[i].kind === 'audiooutput'){ - if (deviceInfos[i].label.replace(/[\W]+/g,"_").toLowerCase().includes(session.outputDevice)){ + if (deviceInfos[i].kind === 'audiooutput') { + if (deviceInfos[i].label.replace(/[\W]+/g, "_").toLowerCase().includes(session.outputDevice)) { session.sink = deviceInfos[i].deviceId; - log("AUDIO OUT DEVICE: "+deviceInfos[i].deviceId); + log("AUDIO OUT DEVICE: " + deviceInfos[i].deviceId); break } } } }); - } catch (e){} + } catch (e) {} } } -if (urlParams.has('fullscreen')){ - session.fullscreen=true; +if (urlParams.has('fullscreen')) { + session.fullscreen = true; } -if (urlParams.has('stats')){ - session.statsMenu=true; +if (urlParams.has('stats')) { + session.statsMenu = true; } -if (urlParams.has('cleandirector') || urlParams.has('cdv')){ +if (urlParams.has('cleandirector') || urlParams.has('cdv')) { session.cleanDirector = true; } -if (urlParams.has('cleanoutput') || urlParams.has('clean') || urlParams.has('cleanish')){ +if (urlParams.has('cleanoutput') || urlParams.has('clean') || urlParams.has('cleanish')) { session.cleanOutput = true; - getById("translateButton").style.display="none"; - getById("credits").style.display="none"; - getById("header").style.display="none"; - getById("controlButtons").style.display="none"; + getById("translateButton").style.display = "none"; + getById("credits").style.display = "none"; + getById("header").style.display = "none"; + getById("controlButtons").style.display = "none"; var style = document.createElement('style'); style.innerHTML = ` video { @@ -1631,31 +1840,31 @@ if (urlParams.has('cleanoutput') || urlParams.has('clean') || urlParams.has('cle document.head.appendChild(style); } -if (urlParams.has('cleanish')){ - session.cleanish=true; +if (urlParams.has('cleanish')) { + session.cleanish = true; } -if (urlParams.has('channels')){ // must be loaded before channelOffset - session.audioChannels = parseInt(urlParams.get('channels')); +if (urlParams.has('channels')) { // must be loaded before channelOffset + session.audioChannels = parseInt(urlParams.get('channels')); session.offsetChannel = 0; log("max channels is 32; channels offset"); - session.audioEffects=true; + session.audioEffects = true; } -if (urlParams.has('channeloffset')){ - session.offsetChannel = parseInt(urlParams.get('channeloffset')); +if (urlParams.has('channeloffset')) { + session.offsetChannel = parseInt(urlParams.get('channeloffset')); log("max channels is 32; channels offset"); - session.audioEffects=true; + session.audioEffects = true; } -if (urlParams.has('enhance')){ +if (urlParams.has('enhance')) { //if (parseInt(urlParams.get('enhance')>0){ - session.enhance = true;//parseInt(urlParams.get('enhance')); + session.enhance = true; //parseInt(urlParams.get('enhance')); //} } -if (urlParams.has('maxviewers') || urlParams.has('mv') ){ - +if (urlParams.has('maxviewers') || urlParams.has('mv')) { + session.maxviewers = urlParams.get('maxviewers') || urlParams.get('mv'); - if (session.maxviewers.length==0){ + if (session.maxviewers.length == 0) { session.maxviewers = 1; } else { session.maxviewers = parseInt(session.maxviewers); @@ -1663,10 +1872,10 @@ if (urlParams.has('maxviewers') || urlParams.has('mv') ){ log("maxviewers set"); } -if (urlParams.has('maxpublishers') || urlParams.has('mp') ){ - +if (urlParams.has('maxpublishers') || urlParams.has('mp')) { + session.maxpublishers = urlParams.get('maxpublishers') || urlParams.get('mp'); - if (session.maxpublishers.length==0){ + if (session.maxpublishers.length == 0) { session.maxpublishers = 1; } else { session.maxpublishers = parseInt(session.maxpublishers); @@ -1674,118 +1883,125 @@ if (urlParams.has('maxpublishers') || urlParams.has('mp') ){ log("maxpublishers set"); } -if (urlParams.has('maxconnections') || urlParams.has('mc') ){ - +if (urlParams.has('maxconnections') || urlParams.has('mc')) { + session.maxconnections = urlParams.get('maxconnections') || urlParams.get('maxconnections'); - if (session.maxconnections.length==0){ + if (session.maxconnections.length == 0) { session.maxconnections = 1; } else { session.maxconnections = parseInt(session.maxconnections); } - + log("maxconnections set"); } -if (urlParams.has('secure')){ +if (urlParams.has('secure')) { session.security = true; - if (!(session.cleanOutput)){ - setTimeout(function() {alert("Enhanced Security Mode Enabled.");}, 100); + if (!(session.cleanOutput)) { + setTimeout(function() { + alert("Enhanced Security Mode Enabled."); + }, 100); } } -if (urlParams.has('random') || urlParams.has('randomize')){ +if (urlParams.has('random') || urlParams.has('randomize')) { session.randomize = true; } -if (urlParams.has('framerate') || urlParams.has('fr') || urlParams.has('fps')){ +if (urlParams.has('framerate') || urlParams.has('fr') || urlParams.has('fps')) { session.framerate = urlParams.get('framerate') || urlParams.get('fr') || urlParams.get('fps'); - session.framerate = parseInt(session.framerate); + session.framerate = parseInt(session.framerate); log("framerate Changed"); log(session.framerate); } - -if (urlParams.has('buffer')){ // needs to be before sync - session.buffer = parseFloat(urlParams.get('buffer')) || 0; - log("buffer Changed: "+session.buffer); - session.sync=0; - session.audioEffects=true; +if (urlParams.has('buffer')) { // needs to be before sync + session.buffer = parseFloat(urlParams.get('buffer')) || 0; + log("buffer Changed: " + session.buffer); + session.sync = 0; + session.audioEffects = true; } -if (urlParams.has('sync')){ - session.sync = parseFloat(urlParams.get('sync')); +if (urlParams.has('sync')) { + session.sync = parseFloat(urlParams.get('sync')); log("sync Changed; in milliseconds. If not set, defaults to auto."); log(session.sync); - session.audioEffects=true; - if (session.buffer===false){ - session.buffer=0; + session.audioEffects = true; + if (session.buffer === false) { + session.buffer = 0; } } -if (urlParams.has('mirror')){ - if (urlParams.get('mirror')=="3"){ +if (urlParams.has('mirror')) { + if (urlParams.get('mirror') == "3") { getById("main").classList.add("mirror"); - } else if (urlParams.get('mirror')=="2"){ + } else if (urlParams.get('mirror') == "2") { session.mirrored = 2; - } else if (urlParams.get('mirror')=="0"){ + } else if (urlParams.get('mirror') == "0") { session.mirrored = 0; - } else if (urlParams.get('mirror')=="false"){ + } else if (urlParams.get('mirror') == "false") { session.mirrored = 0; - } else if (urlParams.get('mirror')=="off"){ + } else if (urlParams.get('mirror') == "off") { session.mirrored = 0; } else { session.mirrored = 1; } } -if (urlParams.has('flip')){ - if (urlParams.get('flip')=="0"){ +if (urlParams.has('flip')) { + if (urlParams.get('flip') == "0") { session.flipped = false; - } else if (urlParams.get('flip')=="false"){ + } else if (urlParams.get('flip') == "false") { session.flipped = false; - } else if (urlParams.get('flip')=="off"){ + } else if (urlParams.get('flip') == "off") { session.flipped = false; } else { session.flipped = true; } } -if ((session.mirrored) && (session.flipped)){ +if ((session.mirrored) && (session.flipped)) { try { log("Mirror all videos"); var mirrorStyle = document.createElement('style'); mirrorStyle.innerHTML = "video {transform: scaleX(-1) scaleY(-1); }"; document.getElementsByTagName("head")[0].appendChild(mirrorStyle); - } catch (e){errorlog(e);} -} else if (session.mirrored){ // mirror the video horizontally + } catch (e) { + errorlog(e); + } +} else if (session.mirrored) { // mirror the video horizontally try { log("Mirror all videos"); var mirrorStyle = document.createElement('style'); - mirrorStyle.innerHTML = "video {transform: scaleX(-1);}"; + mirrorStyle.innerHTML = "video {transform: scaleX(-1);}"; document.getElementsByTagName("head")[0].appendChild(mirrorStyle); - } catch (e){errorlog(e);} -} else if (session.flipped){ // mirror the video vertically + } catch (e) { + errorlog(e); + } +} else if (session.flipped) { // mirror the video vertically try { log("Mirror all videos"); var mirrorStyle = document.createElement('style'); mirrorStyle.innerHTML = "video {transform: scaleY(-1);}"; document.getElementsByTagName("head")[0].appendChild(mirrorStyle); - } catch (e){errorlog(e);} + } catch (e) { + errorlog(e); + } } -if (urlParams.has('icefilter')){ +if (urlParams.has('icefilter')) { log("ICE FILTER ENABLED"); - session.icefilter = urlParams.get('icefilter'); + session.icefilter = urlParams.get('icefilter'); } -if (urlParams.has('effects') || urlParams.has('effect')){ +if (urlParams.has('effects') || urlParams.has('effect')) { session.effects = urlParams.get('effects') || urlParams.get('effect'); session.effects = parseInt(session.effects); - if (session.effects<=0){ + if (session.effects <= 0) { sesson.effects = false; } // mirror == 2 @@ -1793,86 +2009,93 @@ if (urlParams.has('effects') || urlParams.has('effect')){ } -if (urlParams.has('style') || urlParams.has('st')){ +if (urlParams.has('style') || urlParams.has('st')) { session.style = urlParams.get('style') || urlParams.get('st') || 1; - if ((parseInt(session.style)==1 ) || (session.style=="justvideo")){ // no audio only + if ((parseInt(session.style) == 1) || (session.style == "justvideo")) { // no audio only session.style = 1; - } else if ((parseInt(session.style)==2) || (session.style=="waveform")){ // audio waveform + } else if ((parseInt(session.style) == 2) || (session.style == "waveform")) { // audio waveform session.style = 2; - session.audioEffects=true; ////!!!!!!! Do I want to enable the audioEffects myself? or do it here? - } else if ((parseInt(session.style)==3) || (session.style=="volume")){ // photo is taken? upload option? canvas? + session.audioEffects = true; ////!!!!!!! Do I want to enable the audioEffects myself? or do it here? + } else if ((parseInt(session.style) == 3) || (session.style == "volume")) { // photo is taken? upload option? canvas? session.style = 3; - session.audioEffects=true; + session.audioEffects = true; } else { sesson.style = 1; } } -if (urlParams.has('samplerate') || urlParams.has('sr')){ +if (urlParams.has('samplerate') || urlParams.has('sr')) { session.sampleRate = parseInt(urlParams.get('samplerate')) || parseInt(urlParams.get('samplerate')) || 48000; - if (session.audioCtx){ + if (session.audioCtx) { session.audioCtx.close(); // close the default audio context. } - session.audioCtx = new AudioContext({ // create a new audio context with a higher sample rate. - sampleRate: session.sampleRate + session.audioCtx = new AudioContext({ // create a new audio context with a higher sample rate. + sampleRate: session.sampleRate }) - session.audioEffects=true; + session.audioEffects = true; } -if (urlParams.has('noaudioprocessing') || urlParams.has('noap')){ +if (urlParams.has('noaudioprocessing') || urlParams.has('noap')) { session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? session.audioEffects = false; // disable audio inbound effects also. session.audioMeterGuest = false; } -if (urlParams.has('turn')){ +if (urlParams.has('turn')) { var turnstring = urlParams.get('turn'); - if (turnstring=="twilio"){ - try{ + if (turnstring == "twilio") { + try { var request = new XMLHttpRequest(); - request.open('GET', 'https://api.obs.ninja/twilio', false); // `false` makes the request synchronous + request.open('GET', 'https://api.obs.ninja/twilio', false); // `false` makes the request synchronous request.send(null); if (request.status === 200) { log(request.responseText); var res = JSON.parse(request.responseText); - + session.configuration = { - iceServers: [ - { "username": res["1"], - "credential": res["2"], - "url": "turn:global.turn.twilio.com:3478?transport=tcp", - "urls": "turn:global.turn.twilio.com:3478?transport=tcp" - }, - { "username": res["1"], - "credential": res["2"], - "url": "turn:global.turn.twilio.com:443?transport=tcp", - "urls": "turn:global.turn.twilio.com:443?transport=tcp" + iceServers: [{ + "username": res["1"] + , "credential": res["2"] + , "url": "turn:global.turn.twilio.com:3478?transport=tcp" + , "urls": "turn:global.turn.twilio.com:3478?transport=tcp" } - ], - sdpSemantics: 'unified-plan' // future-proofing + , { + "username": res["1"] + , "credential": res["2"] + , "url": "turn:global.turn.twilio.com:443?transport=tcp" + , "urls": "turn:global.turn.twilio.com:443?transport=tcp" + } + ] + , sdpSemantics: 'unified-plan' // future-proofing }; } - } catch(e){errorlog("Twilio Failed");} - - } else if ((turnstring=="false") || (turnstring=="off")){ // disable TURN servers - session.configuration.iceServers = [{ urls: ["stun:stun.l.google.com:19302", "stun:stun4.l.google.com:19302" ]}]; + } catch (e) { + errorlog("Twilio Failed"); + } + + } else if ((turnstring == "false") || (turnstring == "off")) { // disable TURN servers + session.configuration.iceServers = [{ + urls: ["stun:stun.l.google.com:19302", "stun:stun4.l.google.com:19302"] + }]; //session.configuration.iceServers.push(turn); } else { try { turnstring = turnstring.split(";"); - if (turnstring !== "false"){ // false disables the TURN server. Useful for debuggin + if (turnstring !== "false") { // false disables the TURN server. Useful for debuggin var turn = {}; turn.username = turnstring[0]; // myusername - turn.credential = turnstring[1]; //mypassword + turn.credential = turnstring[1]; //mypassword turn.urls = [turnstring[2]]; // ["turn:turn.obs.ninja:443"]; - session.configuration.iceServers = [{ urls: ["stun:stun.l.google.com:19302", "stun:stun4.l.google.com:19302" ]}]; + session.configuration.iceServers = [{ + urls: ["stun:stun.l.google.com:19302", "stun:stun4.l.google.com:19302"] + }]; session.configuration.iceServers.push(turn); } - } catch (e){ - if (!(session.cleanOutput)){ + } catch (e) { + if (!(session.cleanOutput)) { alert("TURN server parameters were wrong."); } errorlog(e); @@ -1881,344 +2104,419 @@ if (urlParams.has('turn')){ } -if (urlParams.has('privacy') || urlParams.has('private') || urlParams.has('relay')){ // please only use if you are also using your own TURN service. +if (urlParams.has('privacy') || urlParams.has('private') || urlParams.has('relay')) { // please only use if you are also using your own TURN service. try { - session.configuration.iceTransportPolicy = "relay"; // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/address - } catch (e){ - if (!(session.cleanOutput)){ + session.configuration.iceTransportPolicy = "relay"; // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/address + } catch (e) { + if (!(session.cleanOutput)) { alert("Privacy mode failed to configure."); } errorlog(e); } + if (session.maxvideobitrate !== false) { + if (session.maxvideobitrate > 2500) { + session.maxvideobitrate = 2500; // Please feel free to get rid of this if using your own TURN servers... + } + } else { + session.maxvideobitrate = 2500; // don't let people pull more than 2500 from you + } + if (session.bitrate !== false) { + if (session.bitrate > 2500) { + session.bitrate = 2500; // Please feel free to get rid of this if using your own TURN servers... + } + } } -if (urlParams.has('wss')){ - if (urlParams.get('wss')){ +if (urlParams.has('wss')) { + if (urlParams.get('wss')) { session.wss = "wss://" + urlParams.get('wss'); } } -window.onmessage = function(e){ // iFRAME support +window.onmessage = function(e) { // iFRAME support log(e); try { - if ("function" in e.data){ // these are calling in-app functions, with perhaps a callback -- TODO: add callbacks + if ("function" in e.data) { // these are calling in-app functions, with perhaps a callback -- TODO: add callbacks var ret = null; - if (e.data.function === "previewWebcam"){ + if (e.data.function === "previewWebcam") { ret = previewWebcam(); - } else if (e.data.function === "changeHTML"){ - try { - ret = getById(e.data.target); - ret.innerHTML = e.data.value; - } catch(e){} - } else if (e.data.function === "publishScreen"){ + } else if (e.data.function === "changeHTML") { + ret = getById(e.data.target); + ret.innerHTML = e.data.value; + } else if (e.data.function === "publishScreen") { ret = publishScreen(); - } + } else if (e.data.function === "eval") { + eval(e.data.value); + } } - } catch (err){errorlog(err);} - - if ("sendChat" in e.data){ + } catch (err) { + errorlog(err); + } + + if ("sendChat" in e.data) { sendChat(e.data.sendChat); // sends to all peers; more options down the road } // Chat out gets called via getChatMessage function // Related code: parent.postMessage({"chat": {"msg":-----,"type":----,"time":---} }, "*"); - - if ("mic" in e.data){ // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. - if (e.data.mic === true){ // unmute + + if ("mic" in e.data) { // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. + if (e.data.mic === true) { // unmute session.muted = false; // set log(session.muted); toggleMute(true); // apply - } else if (e.data.mic === false){ // mute + } else if (e.data.mic === false) { // mute session.muted = true; // set log(session.muted); toggleMute(true); // apply - } else if (e.data.mic === "toggle"){ // toggle + } else if (e.data.mic === "toggle") { // toggle toggleMute(); - } + } } - - if ("camera" in e.data){ // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. - if (e.data.camera === true){ // unmute + + if ("camera" in e.data) { // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. + if (e.data.camera === true) { // unmute session.videoMuted = false; // set log(session.videoMuted); toggleVideoMute(true); // apply - } else if (e.data.camera === false){ // mute + } else if (e.data.camera === false) { // mute session.videoMuted = true; // set log(session.videoMuted); toggleVideoMute(true); // apply - } else if (e.data.camera === "toggle"){ // toggle + } else if (e.data.camera === "toggle") { // toggle toggleVideoMute(); - } - } - - if ("mute" in e.data){ - if (e.data.mute === true){ // unmute - session.speakerMuted = true; // set - toggleSpeakerMute(true); // apply - } else if (e.data.mute === false){ // mute - session.speakerMuted = false; // set - toggleSpeakerMute(true); // apply - } else if (e.data.mute === "toggle"){ // toggle - toggleSpeakerMute(); - } - } else if ("speaker" in e.data){ // same thing as mute. - if (e.data.speaker === true){ // unmute - session.speakerMuted = false; // set - toggleSpeakerMute(true); // apply - } else if (e.data.speaker === false){ // mute - session.speakerMuted = true; // set - toggleSpeakerMute(true); // apply - } else if (e.data.speaker === "toggle"){ // toggle - toggleSpeakerMute(); - } + } } - - - if ("volume" in e.data){ - for (var i in session.rpcs){ + + if ("mute" in e.data) { + if (e.data.mute === true) { // unmute + session.speakerMuted = true; // set + toggleSpeakerMute(true); // apply + } else if (e.data.mute === false) { // mute + session.speakerMuted = false; // set + toggleSpeakerMute(true); // apply + } else if (e.data.mute === "toggle") { // toggle + toggleSpeakerMute(); + } + } else if ("speaker" in e.data) { // same thing as mute. + if (e.data.speaker === true) { // unmute + session.speakerMuted = false; // set + toggleSpeakerMute(true); // apply + } else if (e.data.speaker === false) { // mute + session.speakerMuted = true; // set + toggleSpeakerMute(true); // apply + } else if (e.data.speaker === "toggle") { // toggle + toggleSpeakerMute(); + } + } + + + if ("volume" in e.data) { + for (var i in session.rpcs) { try { session.rpcs[i].videoElement.volume = parseFloat(e.data.volume); - } catch(e){ - errorlog(e); - } - } - } - - - - if ("bitrate" in e.data){ - for (var i in session.rpcs){ - try { - session.requestRateLimit(parseInt(e.data.bitrate),i); - } catch(e){ + } catch (e) { errorlog(e); } } } - - if ("sceneState" in e.data){ // TRUE OR FALSE - tells the connected peers if they are live or not via a tally light change. - + + if ("bitrate" in e.data) { + for (var i in session.rpcs) { + try { + session.requestRateLimit(parseInt(e.data.bitrate), i); + } catch (e) { + errorlog(e); + } + } + } + + if ("sceneState" in e.data) { // TRUE OR FALSE - tells the connected peers if they are live or not via a tally light change. + var visibility = e.data.sceneState; var bundle = {}; bundle.sceneUpdate = []; - - for (var UUID in session.rpcs){ - if (session.rpcs[UUID].visibility!==visibility){ // only move forward if there is a change; the event likes to double fire you see. - + + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].visibility !== visibility) { // only move forward if there is a change; the event likes to double fire you see. + session.rpcs[UUID].visibility = visibility; var msg = {}; msg.visibility = visibility; - - if (session.rpcs[UUID].videoElement.style.display == "none"){ // Flag will be left alone, but message will say its disabled. + + if (session.rpcs[UUID].videoElement.style.display == "none") { // Flag will be left alone, but message will say its disabled. msg.visibility = false; } - + msg.UUID = UUID; - session.sendRequest(msg, UUID); + session.sendRequest(msg, UUID); bundle.sceneUpdate.push(msg) } } session.sendRequest(bundle); // we want all publishing peers to know the state } - - if ("sendMessage" in e.data){ // webrtc send to viewers + + if ("sendMessage" in e.data) { // webrtc send to viewers session.sendMessage(e.data); } - - if ("sendRequest" in e.data){ // webrtc send to publishers + + if ("sendRequest" in e.data) { // webrtc send to publishers session.sendRequest(e.data); } - - if ("sendPeers" in e.data){ // webrtc send message to every connected peer; like send and request; a hammer vs a knife. + + if ("sendPeers" in e.data) { // webrtc send message to every connected peer; like send and request; a hammer vs a knife. session.sendPeers(e.data) } - - if ("reload" in e.data){ - location.reload(); - } - - if ("getStats" in e.data){ - + + if ("reload" in e.data) { + location.reload(); + } + + if ("getStats" in e.data) { + var stats = {}; stats.total_outbound_connections = Object.keys(session.pcs).length; stats.total_inbound_connections = Object.keys(session.rpcs).length; stats.inbound_stats = {}; - for (var i in session.rpcs){ + for (var i in session.rpcs) { stats.inbound_stats[session.rpcs[i].streamID] = session.rpcs[i].stats; } - - - for (var uuid in session.pcs){ - setTimeout(function(UUID){ - session.pcs[UUID].getStats().then(function(stats){ - stats.forEach(stat=>{ - if (stat.type=="outbound-rtp"){ - if (stat.kind=="video"){ - - if ("qualityLimitationReason" in stat){ + + + for (var uuid in session.pcs) { + setTimeout(function(UUID) { + session.pcs[UUID].getStats().then(function(stats) { + stats.forEach(stat => { + if (stat.type == "outbound-rtp") { + if (stat.kind == "video") { + + if ("qualityLimitationReason" in stat) { session.pcs[UUID].stats.quality_Limitation_Reason = stat.qualityLimitationReason; } - if ("framesPerSecond" in stat){ - session.pcs[UUID].stats.resolution = stat.frameWidth+" x "+ stat.frameHeight +" @ "+stat.framesPerSecond; + if ("framesPerSecond" in stat) { + session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond; } - if ("encoderImplementation" in stat){ + if ("encoderImplementation" in stat) { session.pcs[UUID].stats.encoder = stat.encoderImplementation; } - + } + } else if (stat.type == "remote-candidate") { + if ("relayProtocol" in stat) { + if ("ip" in stat) { + session.pcs[UUID].stats.remote_relay_IP = stat.ip; + } + session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol; + } + if ("candidateType" in stat) { + session.pcs[UUID].stats.remote_candidateType = stat.candidateType; + } + } else if (stat.type == "local-candidate") { + if ("relayProtocol" in stat) { + if ("ip" in stat) { + session.pcs[UUID].stats.local_relayIP = stat.ip; + } + session.pcs[UUID].stats.local_relayProtocol = stat.relayProtocol; + } + if ("candidateType" in stat) { + session.pcs[UUID].stats.local_candidateType = stat.candidateType; } } return; }); return; }); - },0,uuid); + }, 0, uuid); } - setTimeout(function(){ + setTimeout(function() { stats.outbound_stats = {}; - for (var i in session.pcs){ + for (var i in session.pcs) { stats.outbound_stats[i] = session.pcs[i].stats; } - parent.postMessage({"stats": stats }, "*"); - },1000); - } - - if ("getLoudness" in e.data){ + parent.postMessage({ + "stats": stats + }, "*"); + }, 1000); + } + + if ("getLoudness" in e.data) { log("GOT LOUDNESS REQUEST"); - if (e.data.getLoudness == true){ + if (e.data.getLoudness == true) { var loudness = {}; - for (var i in session.rpcs){ + for (var i in session.rpcs) { loudness[session.rpcs[i].streamID] = session.rpcs[i].stats.Audio_Loudness; } - parent.postMessage({"loudness": loudness }, "*"); + parent.postMessage({ + "loudness": loudness + }, "*"); session.pushLoudness = true; } else { session.pushLoudness = false; } - } - - if ("getStreamIDs" in e.data){ + } + + if ("getStreamIDs" in e.data) { log("GOT LOUDNESS REQUEST"); - if (e.data.getStreamIDs == true){ + if (e.data.getStreamIDs == true) { var streamIDs = {}; - for (var i in session.rpcs){ + for (var i in session.rpcs) { streamIDs[session.rpcs[i].streamID] = session.rpcs[i].label; } - parent.postMessage({"streamIDs": streamIDs }, "*"); - + parent.postMessage({ + "streamIDs": streamIDs + }, "*"); + } - } - - if ("close" in e.data){ - for (var i in session.rpcs){ + } + + if ("close" in e.data) { + for (var i in session.rpcs) { try { session.rpcs[i].close(); - } catch(e){ + } catch (e) { errorlog(e); } } - } - - if ("style" in e.data){ - try{ + } + + if ("style" in e.data) { + try { const style = document.createElement('style'); - style.textContent = e.data.style; + style.textContent = e.data.style; document.head.append(style); log(style); - } catch(e){ + } catch (e) { errorlog(e); } } - - if ("automixer" in e.data){ - if (e.data.automixer==true){ + + + if ("automixer" in e.data) { + if (e.data.automixer == true) { session.manual = false; - try{ + try { updateMixer(); - } catch(e){}; - } else if (e.data.automixer==false){ + } catch (e) {}; + } else if (e.data.automixer == false) { session.manual = true; } } - - if ("target" in e.data){ + + if ("target" in e.data) { log(e.data); - for (var i in session.rpcs){ + for (var i in session.rpcs) { try { - if ("streamID" in session.rpcs[i]){ - if ((session.rpcs[i].streamID == e.data.target) || ( e.data.target == "*")){ - try{ - if ("settings" in e.data){ - for (const property in e.data.settings){ + if ("streamID" in session.rpcs[i]) { + if ((session.rpcs[i].streamID == e.data.target) || (e.data.target == "*")) { + try { + if ("settings" in e.data) { + for (const property in e.data.settings) { session.rpcs[i].videoElement[property] = e.data.settings[property]; } - } - if ("add" in e.data){ + } + if ("add" in e.data) { getById("gridlayout").appendChild(session.rpcs[i].videoElement); - - } else if ("remove" in e.data){ + + } else if ("remove" in e.data) { try { session.rpcs[i].videoElement.parentNode.removeChild(session.rpcs[i].videoElement); - } catch (e){ - try{ + } catch (e) { + try { session.rpcs[i].videoElement.parentNode.parentNode.removeChild(session.rpcs[i].videoElement.parentNode); - }catch(e){} + } catch (e) {} } } - } catch(e){errorlog(e);} + } catch (e) { + errorlog(e); + } } } - } catch(e){ + } catch (e) { errorlog(e); } } } }; -function pokeIframeAPI(action, value=null, UUID=null){ - try{ + +var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; +var eventer = window[eventMethod]; +var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message"; +eventer(messageEvent, function(e) { // this listens for child IFRAMES. + if ("action" in e.data) { + if (e.data.action == "screen-share-ended") { + if (session.screenShareElement) { + if (e.source == session.screenShareElement.contentWindow) { // reject messages send from other iframes + warnlog(e); + session.screenShareElement.contentWindow.postMessage({ + "close": true + }, '*'); + session.screenShareElement.parentNode.removeChild(session.screenShareElement); + session.screenShareElement = false; + updateMixer(); + getById("screenshare2button").classList.add("float"); + getById("screenshare2button").classList.remove("float2"); + } + } + } + } +}); + + +function pokeIframeAPI(action, value = null, UUID = null) { + if (!isIFrame){return;} + try { var data = {}; - + data.action = action; - - if (value!==null){ + + if (value !== null) { data.value = value; } - if (UUID !==null){ + if (UUID !== null) { data.UUID = UUID; } - - if (parent){ + + if (isIFrame) { parent.postMessage(data, "*"); } - } catch(e){errorlog(e);} + } catch (e) { + errorlog(e); + } } -function jumptoroom(event=null){ - - if (event){ + +function jumptoroom(event = null) { + + if (event) { if (event.which !== 13) { return; } - } - - //var pass = prompt("Enter a password if provided, otherwise just click cancel"); //sanitizePassword(session.password); - //if (pass){ - // session.password = sanitizePassword(pass); - //} - + } + + var arr = window.location.href.split('?'); var roomname = getById("joinroomID").value; roomname = sanitizeRoomName(roomname); - if (roomname.length){ - if (arr.length > 1 && arr[1] !== '') { - window.location+="&room="+roomname; + if (roomname.length) { + + var passStr = ""; + var pass = prompt("Enter a password if provided, otherwise just click cancel"); //sanitizePassword(session.password); + if (pass && pass.length) { + session.password = sanitizePassword(pass); + passStr = "&password=" + session.password; } else { - window.location+="?room="+roomname; + session.password = false; + } + + if (arr.length > 1 && arr[1] !== '') { + window.location += "&room=" + roomname + passStr + } else { + window.location += "?room=" + roomname + passStr } } } -function sleep(ms = 0){ - return new Promise(r => setTimeout(r, ms)); // LOLz! +function sleep(ms = 0) { + return new Promise(r => setTimeout(r, ms)); // LOLz! } @@ -2227,59 +2525,65 @@ function sleep(ms = 0){ ////////// Canvas Effects /////////////// -function drawFrameMirrored(){ +function drawFrameMirrored() { session.canvasCtx.save(); - session.canvasCtx.scale(-1, 1); - session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width*-1, session.canvas.height); - session.canvasCtx.restore(); + session.canvasCtx.scale(-1, 1); + session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * -1, session.canvas.height); + session.canvasCtx.restore(); } -function setupCanvas(eleName){ - if (session.canvas===null){ +function setupCanvas(eleName) { + if (session.canvas === null) { session.canvas = document.createElement("canvas"); - session.canvas.width="1280"; - session.canvas.height="720"; + session.canvas.width = "1280"; + session.canvas.height = "720"; session.canvasCtx = session.canvas.getContext('2d'); session.canvasSource = document.createElement("video"); session.canvasSource.autoplay = true; session.canvasSource.srcObject = new MediaStream(); session.streamSrc = session.canvas.captureStream(30); session.videoElement.srcObject = session.streamSrc; - } + } } -function addTracks(eleName){ - if (session.canvas===null){ +function addTracks(eleName) { + if (session.canvas === null) { session.canvas = document.createElement("canvas"); - session.canvas.width="1280"; - session.canvas.height="720"; + session.canvas.width = "1280"; + session.canvas.height = "720"; session.canvasCtx = session.canvas.getContext('2d'); session.canvasSource = document.createElement("video"); session.canvasSource.autoplay = true; session.canvasSource.srcObject = new MediaStream(); session.streamSrc = session.canvas.captureStream(30); session.videoElement.srcObject = session.streamSrc; - } + } } -function applyEffects(eleName, track, stream){ - - if (session.effects==1){ +function applyEffects(eleName, track, stream) { + + if (session.effects == 1) { setupCanvas(eleName); session.canvasSource.srcObject.addTrack(track, stream); session.canvas.width = track.getSettings().width; session.canvas.height = track.getSettings().height; - if (session.effects==1){ - setTimeout(function(){drawFace();},100); + if (session.effects == 1) { + setTimeout(function() { + drawFace(); + }, 100); } - } else if (session.effects==2){ + } else if (session.effects == 2) { setupCanvas(eleName); session.canvasSource.srcObject.addTrack(track, stream); session.canvas.width = track.getSettings().width; session.canvas.height = track.getSettings().height; - var drawRate = parseInt(1000/track.getSettings().frameRate)+1; - if (session.canvasInterval!==null){clearInterval(session.canvasInterval);} - session.canvasInterval = setInterval(function(){drawFrameMirrored();},drawRate); + var drawRate = parseInt(1000 / track.getSettings().frameRate) + 1; + if (session.canvasInterval !== null) { + clearInterval(session.canvasInterval); + } + session.canvasInterval = setInterval(function() { + drawFrameMirrored(); + }, drawRate); } else { session.streamSrc.addTrack(track, stream); session.videoElement.srcObject.addTrack(track, stream); @@ -2287,13 +2591,13 @@ function applyEffects(eleName, track, stream){ } } -function drawFace(){ - var faceAlignment = (function(){ +function drawFace() { + var faceAlignment = (function() { var vid = session.canvasSource; var canvas = session.canvas; var ctx = session.canvasCtx; - + var canvas_tmp = document.createElement("canvas"); var ctx_tmp = canvas_tmp.getContext('2d'); @@ -2303,17 +2607,17 @@ function drawFace(){ var zoom = 10; var scale = 1; var lastFace = {}; - + var w = vid.videoWidth; var h = vid.videoHeight; - var x = vid.videoWidth/2; - var y = vid.videoHeight/2; + var x = vid.videoWidth / 2; + var y = vid.videoHeight / 2; - lastFace.x = vid.videoWidth/2; - lastFace.y = vid.videoHeight/2; + lastFace.x = vid.videoWidth / 2; + lastFace.y = vid.videoHeight / 2; lastFace.w = vid.videoWidth; lastFace.h = vid.videoHeight; - var yoffset=0; + var yoffset = 0; if (window.FaceDetector == undefined) { @@ -2322,8 +2626,12 @@ function drawFace(){ } else { var faceDetector = new FaceDetector(); //console.log('FaceD Loaded'); - setTimeout(function(){detect();},300); - setTimeout(function(){draw();},33); + setTimeout(function() { + detect(); + }, 300); + setTimeout(function() { + draw(); + }, 33); } canvas.height = vid.videoHeight; @@ -2334,87 +2642,105 @@ function drawFace(){ scale = canvas.width / image.width; lastFace.x = 0; lastFace.y = 0; - lastFace.w = 1280/3/16*zoom; - lastFace.h = 720/3/9*zoom; - - w = 1280/5; - h = 720/5; - x = 1280/2; - y = 720/2 - w*9/zoom/2; - + lastFace.w = 1280 / 3 / 16 * zoom; + lastFace.h = 720 / 3 / 9 * zoom; + + w = 1280 / 5; + h = 720 / 5; + x = 1280 / 2; + y = 720 / 2 - w * 9 / zoom / 2; + + + async function detect() { - async function detect(){ - ctx_tmp.drawImage(vid, 0, 0, vid.videoWidth, vid.videoHeight); image.src = canvas_tmp.toDataURL(); await faceDetector.detect(image).then(faces => { - - if (faces.length===0){ + + if (faces.length === 0) { log("NO FACES"); - setTimeout(function(){detect();},10); + setTimeout(function() { + detect(); + }, 10); return; } for (let face of faces) { - lastFace.x = (face.boundingBox.x+lastFace.x)/2 || face.boundingBox.x; - lastFace.y = (face.boundingBox.y+lastFace.y)/2 || face.boundingBox.y; - lastFace.w = (face.boundingBox.width+lastFace.w)/2 || face.boundingBox.width; - lastFace.h = (face.boundingBox.height+lastFace.h)/2 || face.boundingBox.height; + lastFace.x = (face.boundingBox.x + lastFace.x) / 2 || face.boundingBox.x; + lastFace.y = (face.boundingBox.y + lastFace.y) / 2 || face.boundingBox.y; + lastFace.w = (face.boundingBox.width + lastFace.w) / 2 || face.boundingBox.width; + lastFace.h = (face.boundingBox.height + lastFace.h) / 2 || face.boundingBox.height; } - - setTimeout(function(){detect();},300); + + setTimeout(function() { + detect(); + }, 300); }).catch((e) => { console.error("Boo, Face Detection failed: " + e); }); - + } function draw() { canvas.height = vid.videoHeight; canvas.width = vid.videoWidth; - if (lastFace.w-w<0.15*lastFace.w){ - w = w*0.999 + 0.001*lastFace.w; + if (lastFace.w - w < 0.15 * lastFace.w) { + w = w * 0.999 + 0.001 * lastFace.w; } - if (lastFace.h-h<0.15*lastFace.h){ - h = h*0.999 + 0.001*lastFace.h; + if (lastFace.h - h < 0.15 * lastFace.h) { + h = h * 0.999 + 0.001 * lastFace.h; } - if (Math.abs(x-(lastFace.x+lastFace.w/2))>0.15*(lastFace.x+lastFace.w/2.0)){ - x = x*0.999 + 0.001*(lastFace.x+lastFace.w/2.0); + if (Math.abs(x - (lastFace.x + lastFace.w / 2)) > 0.15 * (lastFace.x + lastFace.w / 2.0)) { + x = x * 0.999 + 0.001 * (lastFace.x + lastFace.w / 2.0); } - if (Math.abs(y-(lastFace.y+lastFace.h/2))>0.15*(lastFace.y+lastFace.h/2.0)){ - y = y*0.999 + 0.001*(lastFace.y+lastFace.h/2.0); + if (Math.abs(y - (lastFace.y + lastFace.h / 2)) > 0.15 * (lastFace.y + lastFace.h / 2.0)) { + y = y * 0.999 + 0.001 * (lastFace.y + lastFace.h / 2.0); + } + + yoffset = w * 9 / zoom / 2; + + var yyy = y - w * 9 / zoom - yoffset; + var hhh = w * 3 * 9 / zoom; + var www = w * 3 * 16 / zoom; + var xxx = x - w * 16 / zoom * 1.5; + + if (www + xxx < 1280) { + xxx = 1280 - www; + } + if (hhh + yyy < 720) { + yyy = 720 - hhh; + } + + if (www + xxx > 1280) { + xxx = 1280 - www; + } + if (hhh + yyy > 720) { + yyy = 720 - hhh; + } + + if (yyy < 0) { + yyy = 0; + } + if (xxx < 0) { + xxx = 0; } - - yoffset = w*9/zoom/2; - var yyy = y-w*9/zoom - yoffset; - var hhh = w*3*9/zoom; - var www = w*3*16/zoom; - var xxx = x-w*16/zoom*1.5; - - if (www+xxx<1280){xxx=1280-www;} - if (hhh+yyy<720){yyy=720-hhh;} - - if (www+xxx>1280){xxx=1280-www;} - if (hhh+yyy>720){yyy=720-hhh;} - - if (yyy<0){yyy=0;} - if (xxx<0){xxx=0;} - ctx.drawImage( - vid, - xxx, - yyy, - www, - hhh, - 0, - 0, - vid.videoWidth, - vid.videoHeight - ); + vid + , xxx + , yyy + , www + , hhh + , 0 + , 0 + , vid.videoWidth + , vid.videoHeight + ); - setTimeout(function(){draw();},30); + setTimeout(function() { + draw(); + }, 30); } })(); } @@ -2422,24 +2748,24 @@ function drawFace(){ //////// END CANVAS EFFECTS /////////////////// -var permaid=false; +var permaid = false; -if (urlParams.has('permaid') || urlParams.has('push')){ - permaid = urlParams.get('push') || urlParams.get('permaid'); +if (urlParams.has('permaid') || urlParams.has('push')) { + permaid = urlParams.get('push') || urlParams.get('permaid'); - if (permaid){ + if (permaid) { session.streamID = sanitizeStreamID(permaid); } else { permaid = null; } - - if (urlParams.has('permaid')){ - updateURL("permaid="+session.streamID, true); // I'm not deleting the permaID first tho... + + if (urlParams.has('permaid')) { + updateURL("permaid=" + session.streamID, true, false); // I'm not deleting the permaID first tho... } else { - updateURL("push="+session.streamID, true); // I'm not deleting the permaID first tho... + updateURL("push=" + session.streamID, true, false); // I'm not deleting the permaID first tho... } - - if (urlParams.has('director')){ // if I do a short form of this, it will cause duplications in the code elsewhere. + + if (urlParams.has('director')) { // if I do a short form of this, it will cause duplications in the code elsewhere. //var director_room_input = urlParams.get('director'); //director_room_input = sanitizeRoomName(director_room_input); //createRoom(director_room_input); @@ -2448,74 +2774,74 @@ if (urlParams.has('permaid') || urlParams.has('push')){ getById("container-1").className = 'column columnfade advanced'; getById("container-4").className = 'column columnfade advanced'; getById("dropButton").className = 'column columnfade advanced'; - + getById("info").innerHTML = ""; - if (session.videoDevice === 0){ + if (session.videoDevice === 0) { getById("add_camera").innerHTML = "Share your Microphone"; - miniTranslate(getById("add_camera"),"share-your-mic"); + miniTranslate(getById("add_camera"), "share-your-mic"); } else { getById("add_camera").innerHTML = "Share your Camera"; - miniTranslate(getById("add_camera"),"share-your-camera"); + miniTranslate(getById("add_camera"), "share-your-camera"); } getById("add_screen").innerHTML = "Share your Screen"; - miniTranslate(getById("add_screen"),"share-your-screen"); - + miniTranslate(getById("add_screen"), "share-your-screen"); + getById("passwordRoom").value = ""; getById("videoname1").value = ""; getById("dirroomid").innerHTML = ""; getById("roomid").innerHTML = ""; - - getById("mainmenu").style.alignSelf= "center"; + + getById("mainmenu").style.alignSelf = "center"; getById("mainmenu").classList.add("mainmenuclass"); - getById("header").style.alignSelf= "center"; - - if ((iOS) || (iPad)){ - getById("header").style.display= "none"; // just trying to free up space. + getById("header").style.alignSelf = "center"; + + if ((iOS) || (iPad)) { + getById("header").style.display = "none"; // just trying to free up space. } - - if (session.webcamonly==true){ // mobile or manual flag 'webcam' pflag set + + if (session.webcamonly == true) { // mobile or manual flag 'webcam' pflag set getById("head1").innerHTML = '- Please accept any camera permissions'; - } else { + } else { getById("head1").innerHTML = '
- Please select which you wish to share'; } } -} +} + +if ((session.roomid) || (urlParams.has('roomid')) || (urlParams.has('r')) || (urlParams.has('room')) || (filename) || (permaid !== false)) { -if ( (session.roomid) || (urlParams.has('roomid')) || (urlParams.has('r')) || (urlParams.has('room')) || (filename) || (permaid!==false)){ - var roomid = ""; - if (filename){ + if (filename) { roomid = filename; - } else if (urlParams.has('room')){ - roomid = urlParams.get('room'); - } else if (urlParams.has('roomid')){ - roomid = urlParams.get('roomid'); - } else if (urlParams.has('r')){ - roomid = urlParams.get('r'); - } else if (session.roomid){ + } else if (urlParams.has('room')) { + roomid = urlParams.get('room'); + } else if (urlParams.has('roomid')) { + roomid = urlParams.get('roomid'); + } else if (urlParams.has('r')) { + roomid = urlParams.get('r'); + } else if (session.roomid) { roomid = session.roomid; } - + session.roomid = sanitizeRoomName(roomid); - - if (!(session.cleanOutput)){ - if (session.roomid==="test"){ - if (session.password===session.defaultPassword){ + + if (!(session.cleanOutput)) { + if (session.roomid === "test") { + if (session.password === session.defaultPassword) { var testRoomResponse = confirm("The room name 'test' is very commonly used and may not be secure.\n\nAre you sure you wish to proceed?"); - if (testRoomResponse==false){ + if (testRoomResponse == false) { hangup(); throw new Error("User requested to not enter room 'room'."); } } } } - - if (session.audioDevice===false){ - getById("headphonesDiv2").style.display="inline-block"; - getById("headphonesDiv").style.display="inline-block"; + + if (session.audioDevice === false) { + getById("headphonesDiv2").style.display = "inline-block"; + getById("headphonesDiv").style.display = "inline-block"; } getById("info").innerHTML = ""; - getById("info").style.color="#CCC"; + getById("info").style.color = "#CCC"; getById("videoname1").value = session.roomid; getById("dirroomid").innerText = session.roomid; getById("roomid").innerText = session.roomid; @@ -2523,40 +2849,40 @@ if ( (session.roomid) || (urlParams.has('roomid')) || (urlParams.has('r')) || (u getById("container-4").className = 'column columnfade advanced'; getById("container-7").style.display = 'none'; getById("container-8").style.display = 'none'; - getById("mainmenu").style.alignSelf= "center"; + getById("mainmenu").style.alignSelf = "center"; getById("mainmenu").classList.add("mainmenuclass"); - getById("header").style.alignSelf= "center"; - - if (session.webcamonly==true){ // mobile or manual flag 'webcam' pflag set + getById("header").style.alignSelf = "center"; + + if (session.webcamonly == true) { // mobile or manual flag 'webcam' pflag set getById("head1").innerHTML = ''; - } else { + } else { getById("head1").innerHTML = 'Please select an option to join.'; } - - if (session.roomid.length>0){ - if (session.videoDevice === 0){ + + if (session.roomid.length > 0) { + if (session.videoDevice === 0) { getById("add_camera").innerHTML = "Join room with Microphone"; - miniTranslate(getById("add_camera"),"join-room-with-mic"); + miniTranslate(getById("add_camera"), "join-room-with-mic"); } else { getById("add_camera").innerHTML = "Join Room with Camera"; - miniTranslate(getById("add_camera"),"join-room-with-camera"); + miniTranslate(getById("add_camera"), "join-room-with-camera"); } getById("add_screen").innerHTML = "Screenshare with Room"; - miniTranslate(getById("add_screen"),"share-screen-with-room"); + miniTranslate(getById("add_screen"), "share-screen-with-room"); } else { - if (session.videoDevice === 0){ + if (session.videoDevice === 0) { getById("add_camera").innerHTML = "Share your Microphone"; - miniTranslate(getById("add_camera"),"share-your-mic"); + miniTranslate(getById("add_camera"), "share-your-mic"); } else { getById("add_camera").innerHTML = "Share your Camera"; - miniTranslate(getById("add_camera"),"share-your-camera"); + miniTranslate(getById("add_camera"), "share-your-camera"); } getById("add_screen").innerHTML = "Share your Screen"; - miniTranslate(getById("add_screen"),"share-your-screen"); + miniTranslate(getById("add_screen"), "share-your-screen"); } getById("head3").className = 'advanced'; - - if (session.scene !== false){ + + if (session.scene !== false) { getById("container-4").className = 'column columnfade'; getById("container-3").className = 'column columnfade'; getById("container-2").className = 'column columnfade'; @@ -2569,55 +2895,61 @@ if ( (session.roomid) || (urlParams.has('roomid')) || (urlParams.has('r')) || (u getById("translateButton").style.display = "none"; log("Update Mixer Event on REsize SET"); window.addEventListener("resize", updateMixer); - window.addEventListener("orientationchange", function(){setTimeout(updateMixer, 200);}); + window.addEventListener("orientationchange", function() { + setTimeout(updateMixer, 200); + }); joinRoom(session.roomid); // this is a scene, so we want high resolutions getById("main").style.overflow = "hidden"; } else { - if ((permaid===null) && (session.roomid=="")){ - if (!(session.cleanOutput)){ + if ((permaid === null) && (session.roomid == "")) { + if (!(session.cleanOutput)) { getById("head3").className = ''; } } } - -} else if (urlParams.has('director')){ // if I do a short form of this, it will cause duplications in the code elsewhere. - if (directorLanding==false){ + +} else if (urlParams.has('director')) { // if I do a short form of this, it will cause duplications in the code elsewhere. + if (directorLanding == false) { var director_room_input = urlParams.get('director'); director_room_input = sanitizeRoomName(director_room_input); - log("director_room_input:"+director_room_input); + log("director_room_input:" + director_room_input); createRoom(director_room_input); } -} else if ((session.view) && (permaid===false)){ +} else if ((session.view) && (permaid === false)) { session.audioMeterGuest = false; - if (session.audioEffects===null){ - session.audioEffects=false; + if (session.audioEffects === null) { + session.audioEffects = false; } log("Update Mixer Event on REsize SET"); getById("translateButton").style.display = "none"; window.addEventListener("resize", updateMixer); - window.addEventListener("orientationchange", function(){setTimeout(updateMixer, 200);}); + window.addEventListener("orientationchange", function() { + setTimeout(updateMixer, 200); + }); getById("main").style.overflow = "hidden"; -} - -if (session.audioEffects===null){ - session.audioEffects=true; } -if (urlParams.has('hidemenu') || urlParams.has('hm')){ // needs to happen the room and permaid applications - getById("mainmenu").style.display="none"; - getById("header").style.display="none"; +if (session.audioEffects === null) { + session.audioEffects = true; +} + +if (urlParams.has('hidemenu') || urlParams.has('hm')) { // needs to happen the room and permaid applications + getById("mainmenu").style.display = "none"; + getById("header").style.display = "none"; getById("mainmenu").style.opacity = 0; getById("header").style.opacity = 0; } -if (urlParams.has('hideheader') || urlParams.has('noheader') || urlParams.has('hh')){ // needs to happen the room and permaid applications - getById("header").style.display="none"; +if (urlParams.has('hideheader') || urlParams.has('noheader') || urlParams.has('hh')) { // needs to happen the room and permaid applications + getById("header").style.display = "none"; getById("header").style.opacity = 0; } -function checkConnection(){ - if (session.ws===null){return;} - if (document.getElementById("qos")){ // true or false; null might cause problems? +function checkConnection() { + if (session.ws === null) { + return; + } + if (document.getElementById("qos")) { // true or false; null might cause problems? if ((session.ws) && (session.ws.readyState === WebSocket.OPEN)) { getById("qos").style.color = "white"; } else { @@ -2625,141 +2957,145 @@ function checkConnection(){ } } } -setInterval(function(){checkConnection();},5000); +setInterval(function() { + checkConnection(); +}, 5000); -function printViewStats(menu, statsObj, streamID){ // Stats for viewing a remote video +function printViewStats(menu, statsObj, streamID) { // Stats for viewing a remote video var scrollLeft = menu.scrollLeft; var scrollTop = menu.scrollTop; //menu.innerHTML="rae:"+session.audioEffects+ ", lae:"+ !session.disableWebAudio; - menu.innerHTML="StreamID: "+streamID+"
"; - menu.innerHTML+= printValues(statsObj); + menu.innerHTML = "StreamID: " + streamID + "
"; + menu.innerHTML += printValues(statsObj); menu.scrollTop = scrollTop; menu.scrollLeft = scrollLeft; - + } -function printValues(obj) { // see: printViewStats +function printValues(obj) { // see: printViewStats var out = ""; for (var key in obj) { if (typeof obj[key] === "object") { - if (obj[key]!=null){ + if (obj[key] != null) { var tmp = key; tmp = sanitizeChat((tmp)); - out += "
  • "+tmp+"

  • " + out += "
  • " + tmp + "

  • " out += printValues(obj[key]); } } else { - if (key.startsWith("_")){ + if (key.startsWith("_")) { // if it starts with _, we don't want to show it. } else { - try{ - var unit = ''; - + try { + var unit = ''; + var value = obj[key]; - + var stat = sanitizeChat(key); - - if (typeof obj[key] == "string"){ + + if (typeof obj[key] == "string") { value = sanitizeChat((value)); } - - if(key == 'Bitrate_in_kbps') { + + if (key == 'Bitrate_in_kbps') { var unit = " kbps"; stat = "Bitrate"; } - if(key == 'type') { + if (key == 'type') { var unit = ""; stat = 'Type'; - - if (value == "Audio Track"){ - value = "🔊 "+value; + + if (value == "Audio Track") { + value = "🔊 " + value; //out += ""; } - - if (value == "Video Track"){ - value = "📺 "+value; + + if (value == "Video Track") { + value = "📺 " + value; } - + } - if(key == 'packetLoss_in_percentage') { + if (key == 'packetLoss_in_percentage') { var unit = " %"; stat = 'Packet Loss 📶'; - value = parseInt(parseFloat(value)*10000)/10000.0; + value = parseInt(parseFloat(value) * 10000) / 10000.0; } - if(key == 'Local_Relay_IP') { - value = ""+value+""; + if (key == 'local_relayIP') { + value = "" + value + ""; } - if(key == 'Remote_Relay_IP') { - value = ""+value+""; + if (key == 'remote_relay_IP') { + value = "" + value + ""; } - if(key == 'Local_Peer_type') { - if (value=="relay"){ + if (key == 'local_candidateType') { + if (value == "relay") { value = "💸 relay server"; } } - if(key == 'Remote_Peer_type') { - if (value=="relay"){ + if (key == 'remote_candidateType') { + if (value == "relay") { value = "💸 relay server"; } } - if(key == 'height_url') { - if (value==false){ + if (key == 'height_url') { + if (value == false) { continue; } } - if(key == 'width_url') { - if (value==false){ + if (key == 'width_url') { + if (value == false) { continue; } } - if(key == 'height_url') { - if (value==false){ + if (key == 'height_url') { + if (value == false) { continue; } } - if(key == 'version') { + if (key == 'version') { stat = "OBS.Ninja Version"; } - if(key == 'platform') { + if (key == 'platform') { stat = "Platform (OS)"; } - if(key == 'aec_url') { + if (key == 'aec_url') { stat = "Echo-Cancellation"; } - if(key == 'agc_url') { + if (key == 'agc_url') { stat = "Auto-Gain (agc)"; } - if(key == 'denoise_url') { + if (key == 'denoise_url') { stat = "De-noising "; } - if(key == 'audio_level') { + if (key == 'audio_level') { stat = "Audio Level"; } - if(key == 'Buffer_Delay_in_ms') { + if (key == 'Buffer_Delay_in_ms') { var unit = " ms"; stat = 'Buffer Delay'; } - if (value===null){ - value="null"; + if (value === null) { + value = "null"; } - if (key == "stereo_url"){ + if (key == "stereo_url") { stat = "Pro-Audio
    (Stereo-mode)"; - if (value==3){ - value="3 (outbound hi-fi)
    Use Headphones"; - } else if (value==1){ - value="1 (in & out hi-fi)
    Use Headphones"; - } else if (value==2){ - value="3 (inbound hi-fi)"; - } else if (value==4){ - value="3 (multichannel)
    Use Headphones"; - } else if (value==5){ - value="5 (auto-mode)
    Use Headphones"; + if (value == 3) { + value = "3 (outbound hi-fi)
    Use Headphones"; + } else if (value == 1) { + value = "1 (in & out hi-fi)
    Use Headphones"; + } else if (value == 2) { + value = "3 (inbound hi-fi)"; + } else if (value == 4) { + value = "3 (multichannel)
    Use Headphones"; + } else if (value == 5) { + value = "5 (auto-mode)
    Use Headphones"; } } - - out +="
  • "+stat+""+value+ unit + "
  • "; - } catch(e){warnlog(e);} + + out += "
  • " + stat + "" + value + unit + "
  • "; + } catch (e) { + warnlog(e); + } } } } @@ -2767,175 +3103,229 @@ function printValues(obj) { // see: printViewStats } -function printMyStats(menu){ // see: setupStatsMenu - - var scrollLeft = getById("menuStatsBox").scrollLeft; - var scrollTop = getById("menuStatsBox").scrollTop; - menu.innerHTML=""; - +function printMyStats(menu) { // see: setupStatsMenu + + var scrollLeft = getById("menuStatsBox").scrollLeft; + var scrollTop = getById("menuStatsBox").scrollTop; + menu.innerHTML = ""; + session.stats.outbound_connections = Object.keys(session.pcs).length; session.stats.inbound_connections = Object.keys(session.rpcs).length; printViewValues(session.stats); - - function printViewValues(obj) { + + function printViewValues(obj) { for (var key in obj) { - if (typeof obj[key] === "object") { + if (typeof obj[key] === "object") { printViewValues(obj[key]); } else { - menu.innerHTML +="
  • "+key+""+obj[key]+"
  • "; + + var stat = sanitizeChat(key); + var value = obj[key]; + if (typeof value == "string") { + value = sanitizeChat((value)); + } + + if (key == 'local_relayIP') { + value = "" + value + ""; + } + if (key == 'remote_relay_IP') { + value = "" + value + ""; + } + if (key == 'local_candidateType') { + if (value == "relay") { + value = "💸 relay server"; + } + } + if (key == 'remote_candidateType') { + if (value == "relay") { + value = "💸 relay server"; + } + } + + menu.innerHTML += "
  • " + stat + "" + value + "
  • "; } } } - menu.innerHTML+=""; - for (var uuid in session.pcs){ - setTimeout(function(UUID){ - session.pcs[UUID].getStats().then(function(stats){ - stats.forEach(stat=>{ - if (stat.type=="outbound-rtp"){ - if (stat.kind=="video"){ - if ("qualityLimitationReason" in stat){ + menu.innerHTML += ""; + for (var uuid in session.pcs) { + setTimeout(function(UUID) { + session.pcs[UUID].getStats().then(function(stats) { + stats.forEach(stat => { + if (stat.type == "outbound-rtp") { + if (stat.kind == "video") { + if ("qualityLimitationReason" in stat) { session.pcs[UUID].stats.quality_Limitation_Reason = stat.qualityLimitationReason; } - if ("framesPerSecond" in stat){ - session.pcs[UUID].stats.resolution = stat.frameWidth+" x "+ stat.frameHeight +" @ "+stat.framesPerSecond; + if ("framesPerSecond" in stat) { + session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond; } - if ("encoderImplementation" in stat){ + if ("encoderImplementation" in stat) { session.pcs[UUID].stats.encoder = stat.encoderImplementation; } } + } else if (stat.type == "remote-candidate") { + if ("relayProtocol" in stat) { + if ("ip" in stat) { + session.pcs[UUID].stats.remote_relay_IP = stat.ip; + } + session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol; + } + if ("candidateType" in stat) { + session.pcs[UUID].stats.remote_candidateType = stat.candidateType; + } + } else if (stat.type == "local-candidate") { + if ("relayProtocol" in stat) { + if ("ip" in stat) { + session.pcs[UUID].stats.local_relayIP = stat.ip; + } + session.pcs[UUID].stats.local_relayProtocol = stat.relayProtocol; + } + if ("candidateType" in stat) { + session.pcs[UUID].stats.local_candidateType = stat.candidateType; + } } return; }); printViewValues(session.pcs[UUID].stats); - menu.innerHTML+="
    "; - try{ + menu.innerHTML += "
    "; + try { getById("menuStatsBox").scrollLeft = scrollLeft; getById("menuStatsBox").scrollTop = scrollTop; - } catch(e){} + } catch (e) {} return; - }).catch(()=>{ + }).catch(() => { printViewValues(session.pcs[UUID].stats); - menu.innerHTML+="
    "; + menu.innerHTML += "
    "; }); - },0,uuid); + }, 0, uuid); } - try{ + try { getById("menuStatsBox").scrollLeft = scrollLeft; getById("menuStatsBox").scrollTop = scrollTop; - } catch(e){} + } catch (e) {} } - -function updateStats(obsvc=false){ +function updateStats(obsvc = false) { log('updateStats - resolution found'); - if (getById('previewWebcam')===null){return;} // Don't show unless preview (or new stats pane is added) + if (getById('previewWebcam') === null) { + return; + } // Don't show unless preview (or new stats pane is added) try { getById("webcamstats").innerHTML = ""; getById('previewWebcam').srcObject.getVideoTracks().forEach( function(track) { - if ((obsvc) && (parseInt(track.getSettings().frameRate)==30)){ - getById("webcamstats").innerHTML = "Video Settings: "+(track.getSettings().width||0) +"x"+(track.getSettings().height||0)+" @ up to 60fps"; + if ((obsvc) && (parseInt(track.getSettings().frameRate) == 30)) { + getById("webcamstats").innerHTML = "Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + " @ up to 60fps"; } else { - getById("webcamstats").innerHTML = "Current Video Settings: "+(track.getSettings().width||0) +"x"+(track.getSettings().height||0)+"@"+(parseInt(track.getSettings().frameRate*10)/10)+"fps"; + getById("webcamstats").innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + "@" + (parseInt(track.getSettings().frameRate * 10) / 10) + "fps"; } } ); - - } catch (e){errorlog(e);} + + } catch (e) { + errorlog(e); + } } -function toggleMute(apply=false){ // TODO: I need to have this be MUTE, toggle, with volume not touched. +function toggleMute(apply = false) { // TODO: I need to have this be MUTE, toggle, with volume not touched. log("muting"); - if (session.director){ - if (!session.directorEnabledPPT){ + if (session.director) { + if (!session.directorEnabledPPT) { log("Director doesn't have PPT enabled yet"); // director has not enabled PTT yet. return; } } - - if (apply){ - session.muted =! session.muted; + + if (apply) { + session.muted = !session.muted; } //try{var ptt = getById("press2talk");} catch(e){var ptt=false;} - - if (session.muted==false){ + + if (session.muted == false) { session.muted = true; - getById("mutetoggle").className="las la-microphone-slash my-float toggleSize"; - getById("mutebutton").className="float2 red"; - if (session.streamSrc){ + getById("mutetoggle").className = "las la-microphone-slash my-float toggleSize"; + getById("mutebutton").className = "float2 red"; + if (session.streamSrc) { session.streamSrc.getAudioTracks().forEach((track) => { - track.enabled = false; + track.enabled = false; }); } //if (ptt){ // ptt.innerHTML = "🔇 Push to Talk"; //} - - } else{ - session.muted=false; - getById("mutetoggle").className="las la-microphone my-float toggleSize"; - getById("mutebutton").className="float"; - if (session.streamSrc){ + + } else { + session.muted = false; + getById("mutetoggle").className = "las la-microphone my-float toggleSize"; + getById("mutebutton").className = "float"; + if (session.streamSrc) { session.streamSrc.getAudioTracks().forEach((track) => { - track.enabled = true; + track.enabled = true; }); } //if (ptt){ // ptt.innerHTML = "🔴 Push to Mute"; //} } + + if (!apply) { // only if they are changing states do we bother to spam. + data = {}; + data.muteState = session.muted; + session.sendMessage(data); + log("SEND DATA"); + } } -function toggleSpeakerMute(apply=false){ // TODO: I need to have this be MUTE, toggle, with volume not touched. +function toggleSpeakerMute(apply = false) { // TODO: I need to have this be MUTE, toggle, with volume not touched. - if (CtrlPressed){ + if (CtrlPressed) { resetupAudioOut(); } - if (apply){ - session.speakerMuted =! session.speakerMuted; + if (apply) { + session.speakerMuted = !session.speakerMuted; } - if (session.speakerMuted == false){ + if (session.speakerMuted == false) { session.speakerMuted = true; - getById("mutespeakertoggle").className="las la-volume-mute my-float toggleSize"; - getById("mutespeakerbutton").className="float2 red"; - + getById("mutespeakertoggle").className = "las la-volume-mute my-float toggleSize"; + getById("mutespeakerbutton").className = "float2 red"; + var sounds = document.getElementsByTagName("video"); - for (var i = 0; i < sounds.length; ++i){ + for (var i = 0; i < sounds.length; ++i) { sounds[i].muted = session.speakerMuted; } - + } else { session.speakerMuted = false; - - getById("mutespeakertoggle").className="las la-volume-up my-float toggleSize"; - getById("mutespeakerbutton").className="float"; - + + getById("mutespeakertoggle").className = "las la-volume-up my-float toggleSize"; + getById("mutespeakerbutton").className = "float"; + var sounds = document.getElementsByTagName("video"); - for (var i = 0; i < sounds.length; ++i){ - - if (sounds[i].id === "videosource"){ // don't unmute ourselves. feedback galore if so. + for (var i = 0; i < sounds.length; ++i) { + + if (sounds[i].id === "videosource") { // don't unmute ourselves. feedback galore if so. continue; - } else if (sounds[i].id === "previewWebcam"){ + } else if (sounds[i].id === "previewWebcam") { continue; - } else if (sounds[i].id === "screenshare"){ + } else if (sounds[i].id === "screenshare") { continue; } else { sounds[i].muted = session.speakerMuted; } } } - - for (var UUID in session.rpcs){ - if (session.rpcs[UUID].videoElement){ - if (UUID === session.directorUUID){ + + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement) { + if (UUID === session.directorUUID) { session.rpcs[UUID].videoElement.muted = false; // unmute director log("MAKE SURE DIRECTOR ISN'T MUTED"); } else { @@ -2943,135 +3333,137 @@ function toggleSpeakerMute(apply=false){ // TODO: I need to have this be MUTE, t } } } - - if ((iOS) || (iPad)){ + + if ((iOS) || (iPad)) { resetupAudioOut(); } } -function toggleChat(event=null){ // TODO: I need to have this be MUTE, toggle, with volume not touched. - if (session.chat==false){ - setTimeout(function(){document.addEventListener("click", toggleChat);},10); - - getById("chatModule").addEventListener("click",function(e){ - e.stopPropagation(); +function toggleChat(event = null) { // TODO: I need to have this be MUTE, toggle, with volume not touched. + if (session.chat == false) { + setTimeout(function() { + document.addEventListener("click", toggleChat); + }, 10); + + getById("chatModule").addEventListener("click", function(e) { + e.stopPropagation(); return false; }); session.chat = true; - getById("chattoggle").className="las la-comment-dots my-float toggleSize"; - getById("chatbutton").className="float2"; + getById("chattoggle").className = "las la-comment-dots my-float toggleSize"; + getById("chatbutton").className = "float2"; getById("chatModule").style.display = "block"; getById("chatInput").focus(); // give it keyboard focus - } else{ - session.chat=false; - getById("chattoggle").className="las la-comment-alt my-float toggleSize"; - getById("chatbutton").className="float"; + } else { + session.chat = false; + getById("chattoggle").className = "las la-comment-alt my-float toggleSize"; + getById("chatbutton").className = "float"; getById("chatModule").style.display = "none"; - + document.removeEventListener("click", toggleChat); - getById("chatModule").removeEventListener("click", function(e){ - e.stopPropagation(); + getById("chatModule").removeEventListener("click", function(e) { + e.stopPropagation(); return false; }); } - if (getById("chatNotification").value){ + if (getById("chatNotification").value) { getById("chatNotification").value = 0; } getById("chatNotification").classList.remove("notification"); } -function directorAdvanced(ele){ +function directorAdvanced(ele) { var target = document.createElement("div"); target.style = "position:absolute;float:left;width:270px;height:222px;background-color:#7E7E7E;"; - + var closeButton = document.createElement("button"); closeButton.innerHTML = " close"; closeButton.style.left = "5px"; - closeButton.style.position = "relative"; - closeButton.onclick = function(){ + closeButton.style.position = "relative"; + closeButton.onclick = function() { target.parentNode.removeChild(target); }; target.appendChild(closeButton); - + var someButton = document.createElement("button"); someButton.innerHTML = " some action "; someButton.style.left = "5px"; - someButton.style.position = "relative"; - someButton.onclick = function(){ - var actionMsg = {}; + someButton.style.position = "relative"; + someButton.onclick = function() { + var actionMsg = {}; session.sendRequest(actionMsg, ele.dataset.UUID); }; target.appendChild(someButton); - + ele.parentNode.appendChild(target); } -function directorSendMessage(ele){ +function directorSendMessage(ele) { var target = document.createElement("div"); target.style = "position:absolute;float:left;width:270px;height:222px;background-color:#7E7E7E;"; - + var inputField = document.createElement("textarea"); inputField.placeholder = "Enter your message here"; - inputField.style.width="255px"; - inputField.style.height="170px"; + inputField.style.width = "255px"; + inputField.style.height = "170px"; inputField.style.margin = "5px 10px 5px 10px"; inputField.style.padding = "5px"; - + target.appendChild(inputField); - + var sendButton = document.createElement("button"); sendButton.innerHTML = " send message "; sendButton.style.left = "5px"; - sendButton.style.position = "relative"; - sendButton.onclick = function(){ - var chatMsg = {}; - chatMsg.chat=inputField.value; - if (sendButton.parentNode.overlay){ + sendButton.style.position = "relative"; + sendButton.onclick = function() { + var chatMsg = {}; + chatMsg.chat = inputField.value; + if (sendButton.parentNode.overlay) { chatMsg.overlay = sendButton.parentNode.overlay; } session.sendRequest(chatMsg, ele.dataset.UUID); - inputField.value=""; + inputField.value = ""; //target.parentNode.removeChild(target); }; - - + + var closeButton = document.createElement("button"); closeButton.innerHTML = " close"; closeButton.style.left = "5px"; - closeButton.style.position = "relative"; - closeButton.onclick = function(){ + closeButton.style.position = "relative"; + closeButton.onclick = function() { inputField.value = ""; target.parentNode.removeChild(target); }; var overlayMsg = document.createElement("span"); - + overlayMsg.style.left = "16px"; overlayMsg.style.top = "6px"; - overlayMsg.style.position = "relative"; + overlayMsg.style.position = "relative"; overlayMsg.innerHTML = ""; - target.overlay=true; - - overlayMsg.onclick = function(e){ + target.overlay = true; + + overlayMsg.onclick = function(e) { log(e.target.parentNode.parentNode); - if (e.target.parentNode.parentNode.overlay===true){ + if (e.target.parentNode.parentNode.overlay === true) { e.target.parentNode.parentNode.overlay = false; e.target.parentNode.innerHTML = ""; } else { - e.target.parentNode.parentNode.overlay=true; + e.target.parentNode.parentNode.overlay = true; e.target.parentNode.innerHTML = ""; } } - - - inputField.addEventListener("keydown",function(e){ - if(e.keyCode == 13){ + + + inputField.addEventListener("keydown", function(e) { + if (e.keyCode == 13) { e.preventDefault(); sendButton.click(); - } else if (e.keyCode == 27){ + } else if (e.keyCode == 27) { e.preventDefault(); - inputField.value=""; + inputField.value = ""; target.parentNode.removeChild(target); } }); @@ -3082,84 +3474,133 @@ function directorSendMessage(ele){ inputField.focus(); inputField.select(); } -function toggleVideoMute(apply=false){ // TODO: I need to have this be MUTE, toggle, with volume not touched. - if (apply){ - session.videoMuted=!session.videoMuted; + +function toggleVideoMute(apply = false) { // TODO: I need to have this be MUTE, toggle, with volume not touched. + if (apply) { + session.videoMuted = !session.videoMuted; } - if (session.videoMuted==false){ + if (session.videoMuted == false) { session.videoMuted = true; - getById("mutevideotoggle").className="las la-eye-slash my-float toggleSize"; - getById("mutevideobutton").className="float2 red"; - if (session.streamSrc){ + getById("mutevideotoggle").className = "las la-eye-slash my-float toggleSize"; + getById("mutevideobutton").className = "float2 red"; + if (session.streamSrc) { session.streamSrc.getVideoTracks().forEach((track) => { - track.enabled = false; + track.enabled = false; }); } - - } else{ - session.videoMuted=false; - - getById("mutevideotoggle").className="las la-eye my-float toggleSize"; - getById("mutevideobutton").className="float"; - if (session.streamSrc){ + + } else { + session.videoMuted = false; + + getById("mutevideotoggle").className = "las la-eye my-float toggleSize"; + getById("mutevideobutton").className = "float"; + if (session.streamSrc) { session.streamSrc.getVideoTracks().forEach((track) => { - track.enabled = true; + track.enabled = true; }); } } } var toggleSettingsState = false; -function toggleSettings(forceShow=false){ // TODO: I need to have this be MUTE, toggle, with volume not touched. - - getById("multiselect-trigger3").dataset.state="0"; + +function toggleSettings(forceShow = false) { // TODO: I need to have this be MUTE, toggle, with volume not touched. + + getById("multiselect-trigger3").dataset.state = "0"; getById("multiselect-trigger3").classList.add('closed'); getById("multiselect-trigger3").classList.remove('open'); getById("chevarrow2").classList.add('bottom'); - - if (toggleSettingsState==true){if (forceShow==true){return;}} // don't close if already open - if (getById("popupSelector").style.display=="none"){ - + + if (toggleSettingsState == true) { + if (forceShow == true) { + enumerateDevices().then(gotDevices2); + return; + } + } // don't close if already open + if (getById("popupSelector").style.display == "none") { + updateConstraintSliders(); - - setTimeout(function(){document.addEventListener("click", toggleSettings);},10); - - getById("popupSelector").addEventListener("click",function(e){ - e.stopPropagation(); + + setTimeout(function() { + document.addEventListener("click", toggleSettings); + }, 10); + + getById("popupSelector").addEventListener("click", function(e) { + e.stopPropagation(); return false; }); - - enumerateDevices().then(gotDevices2).then(function(){}); - - getById("popupSelector").style.display="inline-block" + + if (navigator.userAgent.indexOf('Chrome') != -1) { + try { + navigator.permissions.query({ + name: "camera" + }).then(function(promise) { + if (promise && promise.state) { + if (promise.state == "prompt") { + navigator.mediaDevices.getUserMedia({ + video: true + , audio: false + }).then(function(streamm) { + enumerateDevices().then(gotDevices2).then(function() { + streamm.getTracks().forEach(function(track) { + track.stop(); // clean up? + }); + }); + + }).catch(function(err) { + enumerateDevices().then(gotDevices2).then(function() {}); + }); + } else { + enumerateDevices().then(gotDevices2).then(function() {}); + } + // console.log(promise.state); //"granted", "prompt" or "rejected" + } else { + enumerateDevices().then(gotDevices2).then(function() {}); + } + }); + } catch (e) { + enumerateDevices().then(gotDevices2).then(function() {}); + } + } else { + enumerateDevices().then(gotDevices2).then(function() {}); + } + + getById("popupSelector").style.display = "inline-block" getById("settingsbutton").classList.add("float2"); getById("settingsbutton").classList.remove("float"); - setTimeout(function(){getById("popupSelector").style.right="0px";},1); - toggleSettingsState=true; - } else{ + setTimeout(function() { + getById("popupSelector").style.right = "0px"; + }, 1); + toggleSettingsState = true; + } else { document.removeEventListener("click", toggleSettings); - getById("popupSelector").removeEventListener("click", function(e){ - e.stopPropagation(); + getById("popupSelector").removeEventListener("click", function(e) { + e.stopPropagation(); return false; }); - - getById("popupSelector").style.right="-400px"; - + + getById("popupSelector").style.right = "-400px"; + getById("settingsbutton").classList.add("float"); getById("settingsbutton").classList.remove("float2"); - setTimeout(function(){getById("popupSelector").style.display="none";},200); - toggleSettingsState=false; + setTimeout(function() { + getById("popupSelector").style.display = "none"; + }, 200); + toggleSettingsState = false; } } -function hangup(){ // TODO: I need to have this be MUTE, toggle, with volume not touched. +function hangup() { // TODO: I need to have this be MUTE, toggle, with volume not touched. getById("main").innerHTML = "👋"; - setTimeout(function(){session.hangup();},0); + setTimeout(function() { + session.hangup(); + }, 0); } -function hangup2(){ + +function hangup2() { session.hangupDirector(); getById("miniPerformer").innerHTML = ""; - getById("press2talk").dataset.enabled="false"; + getById("press2talk").dataset.enabled = "false"; getById("screensharebutton").classList.add("advanced"); getById("settingsbutton").classList.add("advanced"); getById("mutebutton").classList.add("advanced"); @@ -3167,94 +3608,101 @@ function hangup2(){ getById("chatbutton").classList.remove("advanced"); getById("controlButtons").style.display = "inherit"; getById("mutespeakerbutton").classList.remove("advanced"); - getById("miniPerformer").innerHTML = ''; + + if (session.showDirector == false) { + getById("miniPerformer").innerHTML = ''; + } else { + getById("miniPerformer").innerHTML = ''; + } getById("miniPerformer").className = ""; } -function hangupComplete(){ +function hangupComplete() { getById("main").innerHTML = "👋"; } -function raisehand(){ - if (session.directorUUID==false){ +function raisehand() { + if (session.directorUUID == false) { log("no director in room yet"); return; } - + var data = {}; data.UUID = session.directorUUID; - + log(data); - if (getById("raisehandbutton").dataset.raised=="0"){ - getById("raisehandbutton").dataset.raised="1"; + if (getById("raisehandbutton").dataset.raised == "0") { + getById("raisehandbutton").dataset.raised = "1"; getById("raisehandbutton").classList.add("raisedHand"); data.chat = "Raised hand"; log("hand raised"); } else { log("hand lowered"); - getById("raisehandbutton").dataset.raised="0"; + getById("raisehandbutton").dataset.raised = "0"; getById("raisehandbutton").classList.remove("raisedHand"); data.chat = "Lowered hand"; } session.sendMessage(data, data.UUID); } -function lowerhand(){ + +function lowerhand() { log("hand lowered"); - getById("raisehandbutton").dataset.raised="0"; + getById("raisehandbutton").dataset.raised = "0"; getById("raisehandbutton").classList.remove("raisedHand"); } -var previousRoom=""; -var stillNeedRoom=true; +var previousRoom = ""; +var stillNeedRoom = true; var transferCancelled = false; -function directMigrate(ele, event){ // everyone in the room will hangup this guest also? I like that idea. What about the STREAM ID? I suppose we don't kick out if the viewID matches. - if (event === false){ - if (previousRoom===null){ // user cancelled in previous callback +function directMigrate(ele, event) { // everyone in the room will hangup this guest also? I like that idea. What about the STREAM ID? I suppose we don't kick out if the viewID matches. + + if (event === false) { + if (previousRoom === null) { // user cancelled in previous callback ele.innerHTML = ' Transfer'; ele.style.backgroundColor = null; return; } - if (transferCancelled===true){ + if (transferCancelled === true) { ele.innerHTML = ' Transfer'; ele.style.backgroundColor = null; return; } migrateRoom = previousRoom - } else if ((event.ctrlKey) || (event.metaKey)){ + } else if ((event.ctrlKey) || (event.metaKey)) { ele.innerHTML = ' Armed'; ele.style.backgroundColor = "#BF3F3F"; - transferCancelled=false; + transferCancelled = false; Callbacks.push([directMigrate, ele, stillNeedRoom]); - stillNeedRoom=false; + stillNeedRoom = false; log("Migrate queued"); return; } else { var migrateRoom = prompt("Transfer guests to room:\n\n(Please note rooms must share the same password)", previousRoom); - stillNeedRoom=true; - if (migrateRoom===null){ // user cancelled + stillNeedRoom = true; + if (migrateRoom === null) { // user cancelled ele.innerHTML = ' Transfer'; ele.style.backgroundColor = null; - transferCancelled=true; + transferCancelled = true; return; } - try{ + try { migrateRoom = sanitizeRoomName(migrateRoom); previousRoom = migrateRoom; - } catch(e){} - + } catch (e) {} + } ele.innerHTML = ' Transfer'; ele.style.backgroundColor = null; - - if (migrateRoom){ + + if (migrateRoom) { previousRoom = migrateRoom; - + var msg = {}; msg.request = "migrate"; - if (session.password){ - return session.generateHash(migrateRoom+session.password+session.salt,16).then(function(rid){ + if (session.password) { + return session.generateHash(migrateRoom + session.password + session.salt, 16).then(function(rid) { var msg = {}; msg.request = "migrate"; msg.roomid = rid; @@ -3270,19 +3718,20 @@ function directMigrate(ele, event){ // everyone in the room will hangup this gu } } } -var stillNeedHangupTarget=1; -function directHangup(ele, event){ // everyone in the room will hangup this guest? I like that idea. - if (event == false){ - if (stillNeedHangupTarget===1){ +var stillNeedHangupTarget = 1; + +function directHangup(ele, event) { // everyone in the room will hangup this guest? I like that idea. + if (event == false) { + if (stillNeedHangupTarget === 1) { var confirmHangup = confirm("Are you sure you wish to disconnect these users?"); stillNeedHangupTarget = confirmHangup; } else { confirmHangup = stillNeedHangupTarget; } - } else if ((event.ctrlKey) || (event.metaKey)){ + } else if ((event.ctrlKey) || (event.metaKey)) { ele.innerHTML = ' ARMED'; ele.style.backgroundColor = "#BF3F3F"; - stillNeedHangupTarget=1; + stillNeedHangupTarget = 1; Callbacks.push([directHangup, ele, false]); log("Hangup queued"); return; @@ -3290,11 +3739,11 @@ function directHangup(ele, event){ // everyone in the room will hangup this gue var confirmHangup = confirm("Are you sure you wish to disconnect this user?"); } - if (confirmHangup){ + if (confirmHangup) { var msg = {}; //msg.request = "sendroom"; msg.hangup = true; - + //msg.target = ele.dataset.UUID; log(msg); log(ele.dataset.UUID); @@ -3306,15 +3755,15 @@ function directHangup(ele, event){ // everyone in the room will hangup this gue } } -function directEnable(ele, event){ // A directing room only is controlled by the Director, with the exception of MUTE. - if (!((event.ctrlKey) || (event.metaKey))){ - if (ele.dataset.enable==1){ +function directEnable(ele, event) { // A directing room only is controlled by the Director, with the exception of MUTE. + if (!((event.ctrlKey) || (event.metaKey))) { + if (ele.dataset.enable == 1) { ele.dataset.enable = 0; ele.className = ""; ele.children[1].innerHTML = "Add to Scene"; - getById("container_"+ele.dataset.UUID).style.backgroundColor = null; + getById("container_" + ele.dataset.UUID).style.backgroundColor = null; } else { - getById("container_"+ele.dataset.UUID).style.backgroundColor = "#649166"; + getById("container_" + ele.dataset.UUID).style.backgroundColor = "#649166"; ele.dataset.enable = 1; ele.className = "pressed"; ele.children[1].innerHTML = "Remove"; @@ -3325,37 +3774,85 @@ function directEnable(ele, event){ // A directing room only is controlled by the //msg.roomid = session.roomid; msg.scene = "1"; // scene msg.action = "display"; - msg.value = ele.dataset.enable; + msg.value = ele.dataset.enable; msg.target = ele.dataset.UUID; - + session.sendMsg(msg); // send to everyone in the room, so they know if they are on air or not. } -function directMute(ele, event){ // A directing room only is controlled by the Director, with the exception of MUTE. +function directMute(ele, event) { // A directing room only is controlled by the Director, with the exception of MUTE. log("mute"); - if (!((event.ctrlKey) || (event.metaKey))){ - if (ele.dataset.mute==0){ + if (!((event.ctrlKey) || (event.metaKey))) { + if (ele.dataset.mute == 0) { ele.dataset.mute = 1; ele.className = ""; ele.children[1].innerHTML = "Mute in scene"; - } else { + } else { ele.dataset.mute = 0; ele.className = "pressed"; ele.children[1].innerHTML = "Un-mute"; - } + } } var msg = {}; msg.request = "sendroom"; //msg.roomid = session.roomid; msg.scene = "1"; msg.action = "mute"; - msg.value = ele.dataset.mute; + msg.value = ele.dataset.mute; msg.target = ele.dataset.UUID; session.sendMsg(msg); // send to everyone in the room, so they know if they are on air or not. } -function remoteLowerhands(UUID){ +function remoteSpeakerMute(ele, event) { + log("speaker mute"); + if (!((event.ctrlKey) || (event.metaKey))) { + if (ele.dataset.mute == 1) { + ele.dataset.mute = 0; + ele.className = ""; + ele.children[1].innerHTML = "deafen guest"; + } else { + ele.dataset.mute = 1; + ele.className = "pressed"; + ele.children[1].innerHTML = "Un-deafen"; + } + } + + var msg = {}; + if (ele.dataset.mute == 0) { + msg.speakerMute = ele.dataset.mute; + } else { + msg.speakerMute = 0; + } + msg.UUID = ele.dataset.UUID; + session.sendRequest(msg, ele.dataset.UUID); +} + +function remoteDisplayMute(ele, event) { + log("display mute"); + if (!((event.ctrlKey) || (event.metaKey))) { + if (ele.dataset.mute == 1) { + ele.dataset.mute = 0; + ele.className = ""; + ele.children[1].innerHTML = "blind guest"; + } else { + ele.dataset.mute = 1; + ele.className = "pressed"; + ele.children[1].innerHTML = "Un-blind"; + } + } + + var msg = {}; + if (ele.dataset.mute == 0) { + msg.displayMute = ele.dataset.mute; + } else { + msg.displayMute = 0; + } + msg.UUID = ele.dataset.UUID; + session.sendRequest(msg, ele.dataset.UUID); +} + +function remoteLowerhands(UUID) { var msg = {}; msg.lowerhand = true; msg.UUID = UUID; @@ -3363,30 +3860,30 @@ function remoteLowerhands(UUID){ } -function remoteMute(ele, event){ +function remoteMute(ele, event) { log("mute"); - if (!((event.ctrlKey) || (event.metaKey))){ - if (ele.dataset.mute==1){ + if (!((event.ctrlKey) || (event.metaKey))) { + if (ele.dataset.mute == 1) { ele.dataset.mute = 0; ele.className = ""; ele.children[1].innerHTML = "mute guest"; - } else { + } else { ele.dataset.mute = 1; ele.className = "pressed"; ele.children[1].innerHTML = "Un-mute guest"; - } + } } - + try { session.rpcs[ele.dataset.UUID].directorMutedState = ele.dataset.mute; var volume = session.rpcs[ele.dataset.UUID].directorVolumeState; - } catch(e){ + } catch (e) { errorlog(e); var volume = 100; } - + var msg = {}; - if (ele.dataset.mute==0){ + if (ele.dataset.mute == 0) { msg.volume = volume; } else { msg.volume = 0; @@ -3395,7 +3892,7 @@ function remoteMute(ele, event){ session.sendRequest(msg, ele.dataset.UUID); } -function directVolume(ele){ // A directing room only is controlled by the Director, with the exception of MUTE. +function directVolume(ele) { // A directing room only is controlled by the Director, with the exception of MUTE. log("volume"); var msg = {}; msg.request = "sendroom"; @@ -3408,11 +3905,11 @@ function directVolume(ele){ // A directing room only is controlled by the Direct } -function remoteVolume(ele){ // A directing room only is controlled by the Director, with the exception of MUTE. +function remoteVolume(ele) { // A directing room only is controlled by the Director, with the exception of MUTE. log("volume"); var msg = {}; var muted = session.rpcs[ele.dataset.UUID].directorMutedState; - if (muted==1){ // 1 is a string, not an int, so == and not ===. this happens in a few places :/ + if (muted == 1) { // 1 is a string, not an int, so == and not ===. this happens in a few places :/ session.rpcs[ele.dataset.UUID].directorVolumeState = ele.value; } else { session.rpcs[ele.dataset.UUID].directorVolumeState = ele.value; @@ -3423,7 +3920,7 @@ function remoteVolume(ele){ // A directing room only is controlled by the Direct } -function sendChat(chatmessage="hi"){ // A directing room only is controlled by the Director, with the exception of MUTE. +function sendChat(chatmessage = "hi") { // A directing room only is controlled by the Director, with the exception of MUTE. log("Chat message"); var msg = {}; msg.chat = chatmessage; @@ -3431,79 +3928,114 @@ function sendChat(chatmessage="hi"){ // A directing room only is controlled by t } var activatedStream = false; -function publishScreen(){ - if( activatedStream == true){return;} - activatedStream = true; - setTimeout(function(){activatedStream=false;},1000); - var title = "ScreenShare";//getById("videoname2").value; +function publishScreen() { + if (activatedStream == true) { + return; + } + activatedStream = true; + setTimeout(function() { + activatedStream = false; + }, 1000); + + var title = "ScreenShare"; //getById("videoname2").value; formSubmitting = false; - + var quality = parseInt(getById("webcamquality2").elements.namedItem("resolution2").value); - - if (session.quality!==false){ - quality=session.quality; // override the user's setting + + if (session.quality !== false) { + quality = session.quality; // override the user's setting } - - if (quality==0){ - var width = {ideal: 1920}; - var height = {ideal: 1080}; - } else if (quality==1){ - var width = {ideal: 1280}; - var height = {ideal: 720}; - } else if (quality==2){ - var width = {ideal: 640}; - var height = {ideal: 360}; - } else if (quality>=3){ // lowest - var width = {ideal: 320}; - var height = {ideal: 180}; + + if (quality == 0) { + var width = { + ideal: 1920 + }; + var height = { + ideal: 1080 + }; + } else if (quality == 1) { + var width = { + ideal: 1280 + }; + var height = { + ideal: 720 + }; + } else if (quality == 2) { + var width = { + ideal: 640 + }; + var height = { + ideal: 360 + }; + } else if (quality >= 3) { // lowest + var width = { + ideal: 320 + }; + var height = { + ideal: 180 + }; } - - if (session.width){ - width = {ideal: session.width}; + + if (session.width) { + width = { + ideal: session.width + }; } - if (session.height){ - height = {ideal: session.height}; + if (session.height) { + height = { + ideal: session.height + }; } var constraints = window.constraints = { audio: { - echoCancellation: false, - autoGainControl: false, - noiseSuppression: false - }, - video: {width: width, height: height, mediaSource: "screen"} + echoCancellation: false + , autoGainControl: false + , noiseSuppression: false + } + , video: { + width: width + , height: height + , mediaSource: "screen" + } }; - - if (session.noiseSuppression === true){ + + if (session.noiseSuppression === true) { constraints.audio.noiseSuppression = true;; // the defaults for screen publishing should be off. } - if (session.autoGainControl === true){ + if (session.autoGainControl === true) { constraints.audio.autoGainControl = true; // the defaults for screen publishing should be off. } - if (session.echoCancellation === true){ + if (session.echoCancellation === true) { constraints.audio.echoCancellation = true; // the defaults for screen publishing should be off. } - - if (session.nocursor){ - constraints.video.cursor = { exact: "none" }; // Not sure this does anything, but whatever. - } - - if (session.framerate!==false){ + + if (session.nocursor) { + constraints.video.cursor = { + exact: "none" + }; // Not sure this does anything, but whatever. + } + + if (session.framerate !== false) { constraints.video.frameRate = session.framerate; } else { - constraints.video.frameRate = {ideal: 60}; + constraints.video.frameRate = { + ideal: 60 + }; } - + var audioSelect = document.querySelector('select#audioSourceScreenshare'); var outputSelect = document.querySelector('select#outputSourceScreenshare'); - - + + session.sink = outputSelect.options[outputSelect.selectedIndex].value; - log("Session SInk: "+session.sink); - if (session.sink=="default"){session.sink=false;} - + log("Session SInk: " + session.sink); + if (session.sink == "default") { + session.sink = false; + } + /* if (session.sink ===false){ if (session.outputDevice){ try { @@ -3518,292 +4050,430 @@ function publishScreen(){ } catch (e){} } } */ - + log("*"); - - - session.publishScreen(constraints, title, audioSelect).then((res)=>{ - if (res==false){return;} // no screen selected - log("streamID is: "+session.streamID); - if (session.transcript){ - setTimeout(function(){setupClosedCaptions();},0); + + session.publishScreen(constraints, title, audioSelect).then((res) => { + if (res == false) { + return; + } // no screen selected + log("streamID is: " + session.streamID); + + if (session.transcript) { + setTimeout(function() { + setupClosedCaptions(); + }, 0); } - - if (!(session.cleanOutput)){ - getById("mutebutton").className="float"; - getById("mutespeakerbutton").className="float"; - getById("chatbutton").className="float"; - getById("mutevideobutton").className="float"; - getById("hangupbutton").className="float"; - if (session.showSettings){ - getById("settingsbutton").className="float"; + //ScreenShareState=true; + if (!(session.cleanOutput)) { + getById("mutebutton").className = "float"; + getById("mutespeakerbutton").classList.remove("advanced"); + //getById("mutespeakerbutton").className="float"; + getById("chatbutton").className = "float"; + getById("mutevideobutton").className = "float"; + getById("hangupbutton").className = "float"; + if (session.showSettings) { + getById("settingsbutton").className = "float"; } - if (session.raisehands){ - getById("raisehandbutton").className="float"; + if (session.raisehands) { + getById("raisehandbutton").className = "float"; } - if (session.recordLocal!==false){ - getById("recordLocalbutton").className="float"; + if (session.recordLocal !== false) { + getById("recordLocalbutton").className = "float"; } - if (screensharebutton){ - getById("screensharebutton").className="float"; + if (screensharebutton) { + getById("screensharebutton").className = "float2"; } - getById("controlButtons").style.display="flex"; + getById("controlButtons").style.display = "flex"; getById("helpbutton").style.display = "inherit"; getById("reportbutton").style.display = ""; } else { - getById("controlButtons").style.display="none"; + getById("controlButtons").style.display = "none"; } + + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("advanced"); + getById("controlButtons").style.display = "inherit"; + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("advanced"); + } + getById("head1").className = 'advanced'; getById("head2").className = 'advanced'; - }).catch(()=>{}); + }).catch(() => {}); } -function publishWebcam(btn = false){ - if (btn){ - if (btn.dataset.ready == "false"){ + +function publishWebcam(btn = false) { + if (btn) { + if (btn.dataset.ready == "false") { warnlog("Clicked too quickly; button not enabled yet"); return; } } - - if( activatedStream == true){return;} + + if (activatedStream == true) { + return; + } activatedStream = true; log("PRESSED PUBLISH WEBCAM!!"); - + var title = "Webcam"; // getById("videoname3").value; var ele = getById("previewWebcam"); formSubmitting = false; window.scrollTo(0, 0); // iOS has a nasty habit of overriding the CSS when changing camaera selections, so this addresses that. - + getById("head2").className = 'advanced'; - - if (session.roomid!==false){ - if ((session.roomid==="") && ((!(session.view)) || (session.view===""))){ + + if (session.roomid !== false) { + if ((session.roomid === "") && ((!(session.view)) || (session.view === ""))) { // no room, no viewing, viewing disabled - session.manual=true; + session.manual = true; window.addEventListener("resize", updateMixer); - window.addEventListener("orientationchange", function(){setTimeout(updateMixer, 200);}); + window.addEventListener("orientationchange", function() { + setTimeout(updateMixer, 200); + }); } else { log("ROOM ID ENABLED"); log("Update Mixer Event on REsize SET"); window.addEventListener("resize", updateMixer); - window.addEventListener("orientationchange", function(){setTimeout(updateMixer, 200);}); + window.addEventListener("orientationchange", function() { + setTimeout(updateMixer, 200); + }); getById("main").style.overflow = "hidden"; //session.cbr=0; // we're just going to override it - - if (session.stereo==5){ - if (session.roomid===""){ - session.stereo=1; + + if (session.stereo == 5) { + if (session.roomid === "") { + session.stereo = 1; } else { - session.stereo=3; + session.stereo = 3; } } joinRoom(session.roomid); - if (session.roomid!==""){ - if (!(session.cleanOutput)){ + if (session.roomid !== "") { + if (!(session.cleanOutput)) { getById("head2").className = ''; } } getById("head3").className = 'advanced'; log("4"); } - + } else { getById("head3").className = ''; getById("logoname").style.display = 'none'; } - - log("streamID is: "+session.streamID); - getById("head1").className = 'advanced'; - - if (!(session.cleanOutput)){ - getById("mutebutton").className="float"; - getById("mutespeakerbutton").className="float"; - getById("chatbutton").className="float"; - getById("mutevideobutton").className="float"; - getById("hangupbutton").className="float"; - if (session.showSettings){ - getById("settingsbutton").className="float"; + log("streamID is: " + session.streamID); + getById("head1").className = 'advanced'; + + + if (!(session.cleanOutput)) { + getById("mutebutton").className = "float"; + getById("mutespeakerbutton").classList.remove("advanced"); + //getById("mutespeakerbutton").className="float"; + getById("chatbutton").className = "float"; + getById("mutevideobutton").className = "float"; + getById("hangupbutton").className = "float"; + if (session.showSettings) { + getById("settingsbutton").className = "float"; } - if (session.raisehands){ - getById("raisehandbutton").className="float"; + if (session.raisehands) { + getById("raisehandbutton").className = "float"; } - if (session.recordLocal!==false){ - getById("recordLocalbutton").className="float"; + if (session.recordLocal !== false) { + getById("recordLocalbutton").className = "float"; } - if (screensharebutton){ - getById("screensharebutton").className="float"; + if (screensharebutton) { + if (session.roomid) { + getById("screenshare2button").className = "float"; + getById("screensharebutton").className = "float advanced"; + } else { + getById("screensharebutton").className = "float"; + getById("screenshare2button").className = "float advanced"; + } } - getById("controlButtons").style.display="flex"; + getById("controlButtons").style.display = "flex"; getById("helpbutton").style.display = "inherit"; getById("reportbutton").style.display = ""; } else { - getById("controlButtons").style.display="none"; + getById("controlButtons").style.display = "none"; } - - if (urlParams.has('permaid')){ - updateURL("permaid="+session.streamID); + + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("advanced"); + getById("controlButtons").style.display = "inherit"; + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("advanced"); + } + + if (urlParams.has('permaid')) { + updateURL("permaid=" + session.streamID); } else { - updateURL("push="+session.streamID); + updateURL("push=" + session.streamID); } - + session.publishStream(ele, title); } -function outboundAudioPipeline(stream){ - if (session.disableWebAudio){ +function outboundAudioPipeline(stream) { + if (session.disableWebAudio) { return stream; } - + //if ((iOS) || (iPad)){ // return stream //} else { try { log("Web Audio"); var tracks = stream.getAudioTracks(); - if (tracks.length){ - for (var waid in session.webAudios){ // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){.. - session.webAudios[waid].stop(); - delete session.webAudios[waid]; + if (tracks.length) { + for (var waid in session.webAudios) { // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){.. + session.webAudios[waid].stop(); + delete session.webAudios[waid]; } - - + + var webAudio = {}; webAudio.compressor = false; webAudio.analyser = false; webAudio.gainNode = false; - + webAudio.lowEQ = false; webAudio.midEQ = false; webAudio.highEQ = false; - + webAudio.id = tracks[0].id; // first track is used. - - if (session.audioLatency!==false){ // session.audioLatency could be useful for fixing clicking issues? + + if (session.audioLatency !== false) { // session.audioLatency could be useful for fixing clicking issues? var audioContext = new AudioContext({ - latencyHint: session.audioLatency/1000.0//, // needs to be in seconds, but OBSN user input is via milliseconds - // sampleRate: 48000 // not sure this is a great idea, but might as well add this here, versus later on since it is needed anyways. + latencyHint: session.audioLatency / 1000.0 //, // needs to be in seconds, but OBSN user input is via milliseconds + // sampleRate: 48000 // not sure this is a great idea, but might as well add this here, versus later on since it is needed anyways. }); } else { var audioContext = new AudioContext(); } - + webAudio.audioContext = audioContext; webAudio.mediaStreamSource = audioContext.createMediaStreamSource(stream); // clone to fix iOS issue webAudio.destination = audioContext.createMediaStreamDestination(); webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, audioContext); - + var anonNode = webAudio.gainNode; - - if (session.equalizer){ // https://webaudioapi.com/samples/frequency-response/ for a tool to help set values + + if (session.audioInputChannels == 1) { + webAudio.splitter = audioContext.createChannelSplitter(6); + anonNode.connect(webAudio.splitter); + + webAudio.merger = audioContext.createChannelMerger(6); + webAudio.splitter.connect(webAudio.merger, 0, 0); + webAudio.splitter.connect(webAudio.merger, 0, 1); + webAudio.splitter.connect(webAudio.merger, 0, 2); + webAudio.splitter.connect(webAudio.merger, 0, 3); + webAudio.splitter.connect(webAudio.merger, 0, 4); + webAudio.splitter.connect(webAudio.merger, 0, 5); + anonNode = webAudio.merger; + } + + + if (session.lowcut) { // https://webaudioapi.com/samples/frequency-response/ for a tool to help set values + webAudio.lowcut1 = audioContext.createBiquadFilter(); + webAudio.lowcut1.type = "highpass"; + webAudio.lowcut1.frequency.value = session.lowcut; + + webAudio.lowcut2 = audioContext.createBiquadFilter(); + webAudio.lowcut2.type = "highpass"; + webAudio.lowcut2.frequency.value = session.lowcut; + + webAudio.lowcut3 = audioContext.createBiquadFilter(); + webAudio.lowcut3.type = "highpass"; + webAudio.lowcut3.frequency.value = session.lowcut; + + anonNode.connect(webAudio.lowcut1); + webAudio.lowcut1.connect(webAudio.lowcut2); + webAudio.lowcut2.connect(webAudio.lowcut3); + anonNode = webAudio.lowcut3; + } + + + if (session.equalizer) { // https://webaudioapi.com/samples/frequency-response/ for a tool to help set values webAudio.lowEQ = audioContext.createBiquadFilter(); webAudio.lowEQ.type = "lowshelf"; webAudio.lowEQ.frequency.value = 100; webAudio.lowEQ.gain.value = 0; - + webAudio.midEQ = audioContext.createBiquadFilter(); webAudio.midEQ.type = "peaking"; webAudio.midEQ.frequency.value = 1000; webAudio.midEQ.Q.value = 0.5; webAudio.midEQ.gain.value = 0; - + webAudio.highEQ = audioContext.createBiquadFilter(); webAudio.highEQ.type = "highshelf"; webAudio.highEQ.frequency.value = 10000; webAudio.highEQ.gain.value = 0; - + anonNode.connect(webAudio.lowEQ); webAudio.lowEQ.connect(webAudio.midEQ); webAudio.midEQ.connect(webAudio.highEQ); anonNode = webAudio.highEQ; - } - - if (session.compressor===1){ - webAudio.compressor = audioCompressor(anonNode, audioContext); + } + + if (session.compressor === 1) { + webAudio.compressor = audioCompressor(anonNode, audioContext); anonNode = webAudio.compressor; - } else if (session.compressor===2){ - webAudio.compressor = audioLimiter(anonNode, audioContext); + } else if (session.compressor === 2) { + webAudio.compressor = audioLimiter(anonNode, audioContext); anonNode = webAudio.compressor; - } - + } + webAudio.analyser = audioMeter(anonNode, audioContext); webAudio.analyser.connect(webAudio.destination); - - webAudio.stop = function(){ - try{webAudio.destination.disconnect();}catch(e){} - try{clearInterval(webAudio.analyser.interval);}catch(e){} - try{webAudio.analyser.disconnect();}catch(e){} - try{webAudio.gainNode.disconnect();}catch(e){} - try{webAudio.compressor.disconnect();}catch(e){} - try{webAudio.mediaStreamSource.context.close();}catch(e){} + + webAudio.stop = function() { + try { + webAudio.destination.disconnect(); + } catch (e) {} + try { + clearInterval(webAudio.analyser.interval); + } catch (e) {} + try { + webAudio.analyser.disconnect(); + } catch (e) {} + try { + webAudio.splitter.disconnect(); + } catch (e) {} + try { + webAudio.merger.disconnect(); + } catch (e) {} + try { + webAudio.lowcut1.disconnect(); + webAudio.lowcut2.disconnect(); + webAudio.lowcut3.disconnect(); + } catch (e) {} + try { + webAudio.lowEQ.disconnect(); + } catch (e) {} + try { + webAudio.midEQ.disconnect(); + } catch (e) {} + try { + webAudio.highEQ.disconnect(); + } catch (e) {} + try { + webAudio.gainNode.disconnect(); + } catch (e) {} + try { + webAudio.compressor.disconnect(); + } catch (e) {} + try { + webAudio.mediaStreamSource.context.close(); + } catch (e) {} } - - webAudio.mediaStreamSource.onended = function(){ + + webAudio.mediaStreamSource.onended = function() { webAudio.stop(); }; - + session.webAudios[webAudio.id] = webAudio; - - stream.getTracks().forEach(function(track){ - if (webAudio.id!=track.id){ + + stream.getTracks().forEach(function(track) { + if (webAudio.id != track.id) { webAudio.destination.stream.addTrack(track, stream); } }); - + return webAudio.destination.stream; } else { return stream; // no audio track } - } catch(e){ + } catch (e) { errorlog(e); return stream; } } -function changeLowEQ(lowEQ, trackid=0){ - if (trackid!=0){ +function changeLowCut(freq, trackid = 0) { + if (trackid != 0) { errorlog("EQ Doesn't work for anything but track 0. yet"); } log("LOW EQ"); - - for ( var webAudio in session.webAudios){ - if (!session.webAudios[webAudio].lowEQ){errorlog("EQ not setup");return;} + + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].lowcut1) { + errorlog("EQ not setup"); + return; + } + if (!session.webAudios[webAudio].lowcut2) { + errorlog("EQ not setup"); + return; + } + if (!session.webAudios[webAudio].lowcut3) { + errorlog("EQ not setup"); + return; + } + session.webAudios[webAudio].lowcut1.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); + session.webAudios[webAudio].lowcut2.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); + session.webAudios[webAudio].lowcut3.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); + } + +} + +function changeLowEQ(lowEQ, trackid = 0) { + if (trackid != 0) { + errorlog("EQ Doesn't work for anything but track 0. yet"); + } + log("LOW EQ"); + + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].lowEQ) { + errorlog("EQ not setup"); + return; + } session.webAudios[webAudio].lowEQ.gain.setValueAtTime(lowEQ, session.webAudios[webAudio].audioContext.currentTime); } - + } -function changeMidEQ(midEQ, trackid=0){ - if (trackid!=0){ + +function changeMidEQ(midEQ, trackid = 0) { + if (trackid != 0) { errorlog("EQ Doesn't work for anything but track 0. yet"); } - - for ( var webAudio in session.webAudios){ - if (!session.webAudios[webAudio].midEQ){errorlog("EQ not setup");return;} + + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].midEQ) { + errorlog("EQ not setup"); + return; + } session.webAudios[webAudio].midEQ.gain.setValueAtTime(midEQ, session.webAudios[webAudio].audioContext.currentTime); } - + } -function changeHighEQ(highEQ, trackid=0){ - if (trackid!=0){ + +function changeHighEQ(highEQ, trackid = 0) { + if (trackid != 0) { errorlog("EQ Doesn't work for anything but track 0. yet"); } - - for ( var webAudio in session.webAudios){ - if (!session.webAudios[webAudio].highEQ){errorlog("EQ not setup");return;} + + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].highEQ) { + errorlog("EQ not setup"); + return; + } session.webAudios[webAudio].highEQ.gain.setValueAtTime(highEQ, session.webAudios[webAudio].audioContext.currentTime); } - + } -function audioGainNode(mediaStreamSource, audioContext){ +function audioGainNode(mediaStreamSource, audioContext) { var gainNode = audioContext.createGain(); - if (session.audioGain!==false){ - var gain = parseFloat(session.audioGain/100.0) || 0; + if (session.audioGain !== false) { + var gain = parseFloat(session.audioGain / 100.0) || 0; } else { var gain = 1.0; } @@ -3812,54 +4482,60 @@ function audioGainNode(mediaStreamSource, audioContext){ return gainNode; } -function audioMeter(mediaStreamSource, audioContext){ +function audioMeter(mediaStreamSource, audioContext) { var analyser = audioContext.createAnalyser(); mediaStreamSource.connect(analyser); analyser.fftSize = 256; analyser.smoothingTimeConstant = 0.05; - + var bufferLength = analyser.frequencyBinCount; var dataArray = new Uint8Array(bufferLength); - - + + function draw() { analyser.getByteFrequencyData(dataArray); var total = 0; - for (var i = 0; i < dataArray.length; i++){ + for (var i = 0; i < dataArray.length; i++) { total += dataArray[i]; } - total = total/100; - - if (document.getElementById("meter1")){ - if (total==0){ + total = total / 100; + + if (document.getElementById("meter1")) { + if (total == 0) { getById("meter1").style.width = "1px"; getById("meter2").style.width = "0px"; - } else if (total<=1){ + } else if (total <= 1) { getById("meter1").style.width = "1px"; getById("meter2").style.width = "0px"; - } else if (total<=150){ - getById("meter1").style.width = total+"px"; + } else if (total <= 150) { + getById("meter1").style.width = total + "px"; getById("meter2").style.width = "0px"; - } else if (total>150){ - if (total>200){total=200;} + } else if (total > 150) { + if (total > 200) { + total = 200; + } getById("meter1").style.width = "150px"; - getById("meter2").style.width = (total-150)+"px"; + getById("meter2").style.width = (total - 150) + "px"; + } + } else if (document.getElementById("mutetoggle")) { + if (total > 200) { + total = 200; } - } else if (document.getElementById("mutetoggle")){ - if (total>200){total=200;} total = parseInt(total); - document.getElementById("mutetoggle").style.color = "rgb("+(255-total)+",255,"+(255-total)+")"; + document.getElementById("mutetoggle").style.color = "rgb(" + (255 - total) + ",255," + (255 - total) + ")"; } else { clearInterval(analyser.interval); warnlog("METERS NOT FOUND"); return; } - }; - analyser.interval = setInterval(function(){draw();},100); + }; + analyser.interval = setInterval(function() { + draw(); + }, 100); return analyser; } -function audioCompressor(mediaStreamSource, audioContext){ +function audioCompressor(mediaStreamSource, audioContext) { var compressor = audioContext.createDynamicsCompressor(); compressor.threshold.value = -50; compressor.knee.value = 40; @@ -3870,7 +4546,7 @@ function audioCompressor(mediaStreamSource, audioContext){ return compressor; } -function audioLimiter(mediaStreamSource, audioContext){ +function audioLimiter(mediaStreamSource, audioContext) { var compressor = audioContext.createDynamicsCompressor(); compressor.threshold.value = -5; compressor.knee.value = 0; @@ -3881,35 +4557,35 @@ function audioLimiter(mediaStreamSource, audioContext){ return compressor; } -function activeSpeaker(){ - +function activeSpeaker() { + var changed = false; - for (var UUID in session.rpcs){ - if(session.rpcs[UUID].stats.Audio_Loudness_average){ - session.rpcs[UUID].stats.Audio_Loudness_average = parseFloat(session.rpcs[UUID].stats.Audio_Loudness + session.rpcs[UUID].stats.Audio_Loudness_average)/2; + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].stats.Audio_Loudness_average) { + session.rpcs[UUID].stats.Audio_Loudness_average = parseFloat(session.rpcs[UUID].stats.Audio_Loudness + session.rpcs[UUID].stats.Audio_Loudness_average) / 2; } else { session.rpcs[UUID].stats.Audio_Loudness_average = 1; } log(session.rpcs[UUID].stats.Audio_Loudness_average); - - if (session.rpcs[UUID].stats.Audio_Loudness_average>10){ - if (session.rpcs[UUID].videoElement.style.display!="block"){ - session.rpcs[UUID].videoElement.style.display="block"; - changed=true; + + if (session.rpcs[UUID].stats.Audio_Loudness_average > 10) { + if (session.rpcs[UUID].videoElement.style.display != "block") { + session.rpcs[UUID].videoElement.style.display = "block"; + changed = true; } } else { - if (session.rpcs[UUID].videoElement){ - if (session.rpcs[UUID].videoElement.style.display!="none"){ - changed=true; - session.rpcs[UUID].videoElement.style.display="none"; - } + if (session.rpcs[UUID].videoElement) { + if (session.rpcs[UUID].videoElement.style.display != "none") { + changed = true; + session.rpcs[UUID].videoElement.style.display = "none"; + } } else { - session.rpcs[UUID].videoElement.style.display="none"; + session.rpcs[UUID].videoElement.style.display = "none"; } } - + } - if (changed){ + if (changed) { updateMixer(); } } @@ -3917,192 +4593,220 @@ function activeSpeaker(){ function randomizeArray(unshuffled) { - - var arr = unshuffled.map((a) => ({sort: Math.random(), value: a})).sort((a, b) => a.sort - b.sort).map((a) => a.value); // shuffle once - - for (var i = arr.length - 1; i > 0; i--) { // shuffle twice - var j = Math.floor(Math.random() * (i + 1)); - var tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } + + var arr = unshuffled.map((a) => ({ + sort: Math.random() + , value: a + })).sort((a, b) => a.sort - b.sort).map((a) => a.value); // shuffle once + + for (var i = arr.length - 1; i > 0; i--) { // shuffle twice + var j = Math.floor(Math.random() * (i + 1)); + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } return arr } -function joinRoom(roomname){ - if (roomname.length){ +function joinRoom(roomname) { + if (roomname.length) { roomname = sanitizeRoomName(roomname); log("Join room"); log(roomname); - session.joinRoom(roomname).then(function(response){ // callback from server; we've joined the room. Just the listing is returned - - if (session.joiningRoom === "seedPlz"){ // allow us to seed, now that we have joined the room. + session.joinRoom(roomname).then(function(response) { // callback from server; we've joined the room. Just the listing is returned + + if (session.joiningRoom === "seedPlz") { // allow us to seed, now that we have joined the room. session.joiningRoom = false; // joined session.seedStream(); } else { - session.joiningRoom = false; // no seeding callback + session.joiningRoom = false; // no seeding callback } log("Members in Room"); log(response); - - if (session.randomize===true){ + + if (session.randomize === true) { response = randomizeArray(response); log("Randomized List of Viewers"); log(response); - for (var i in response){ - if ("UUID" in response[i]){ - if ("streamID" in response[i]){ - if (response[i].UUID in session.rpcs){ + for (var i in response) { + if ("UUID" in response[i]) { + if ("streamID" in response[i]) { + if (response[i].UUID in session.rpcs) { log("RTC already connected"); /// lets just say instead of Stream, we have } else { log(response[i].streamID); var streamID = session.desaltStreamID(response[i].streamID); - log("STREAM ID DESALTED 3: "+streamID); - setTimeout(function(sid){play(sid);} ,(Math.floor(Math.random()*100)), streamID); // add some furtherchance with up to 100ms added latency + log("STREAM ID DESALTED 3: " + streamID); + setTimeout(function(sid) { + play(sid); + }, (Math.floor(Math.random() * 100)), streamID); // add some furtherchance with up to 100ms added latency } } } } } else { - for (var i in response){ - if ("UUID" in response[i]){ - if ("streamID" in response[i]){ - if (response[i].UUID in session.rpcs){ + for (var i in response) { + if ("UUID" in response[i]) { + if ("streamID" in response[i]) { + if (response[i].UUID in session.rpcs) { log("RTC already connected"); /// lets just say instead of Stream, we have } else { log(response[i].streamID); var streamID = session.desaltStreamID(response[i].streamID); - log("STREAM ID DESALTED 3: "+streamID); - play(streamID); // play handles the group room mechanics here + log("STREAM ID DESALTED 3: " + streamID); + play(streamID); // play handles the group room mechanics here } } } } } - },function(error){return {};}); + }, function(error) { + return {}; + }); } else { log("Room name not long enough or contained all bad characaters"); - } + } } -function createRoom(roomname=false){ - - if (roomname==false){ +function createRoom(roomname = false) { + + if (roomname == false) { roomname = getById("videoname1").value; roomname = sanitizeRoomName(roomname); - if (roomname.length!=0){ - updateURL("director="+roomname, true); // make the link reloadable. + if (roomname.length != 0) { + updateURL("director=" + roomname, true, false); // make the link reloadable. } } - if (roomname.length==0){ - if (!(session.cleanOutput)){ + if (roomname.length == 0) { + if (!(session.cleanOutput)) { alert("Please enter a room name before continuing"); } return; } log(roomname); session.roomid = roomname; - - getById("dirroomid").innerHTML = decodeURIComponent(session.roomid); + + getById("dirroomid").innerHTML = decodeURIComponent(session.roomid); getById("roomid").innerHTML = session.roomid; - + var passwordRoom = getById("passwordRoom").value; passwordRoom = sanitizePassword(passwordRoom); - if (passwordRoom.length){ - session.password=passwordRoom; + if (passwordRoom.length) { + session.password = passwordRoom; session.defaultPassword = false; - - if (urlParams.has('pass')){ - updateURL("pass="+session.password); - } else if (urlParams.has('pw')){ - updateURL("pw="+session.password); - } else if (urlParams.has('p')){ - updateURL("p="+session.password); - } else { - updateURL("password="+session.password); + + if (urlParams.has('pass')) { + updateURL("pass=" + session.password); + } else if (urlParams.has('pw')) { + updateURL("pw=" + session.password); + } else if (urlParams.has('p')) { + updateURL("p=" + session.password); + } else { + updateURL("password=" + session.password); } } - - var passAdd=""; - var passAdd2=""; - - if ((session.defaultPassword === false) && (session.password)){ - passAdd2="&password="+session.password; - return session.generateHash(session.password+session.salt,4).then(function(hash){ - passAdd="&hash="+hash; - createRoomCallback(passAdd, passAdd2); - }); + + var passAdd = ""; + var passAdd2 = ""; + + if ((session.defaultPassword === false) && (session.password)) { + passAdd2 = "&password=" + session.password; + return session.generateHash(session.password + session.salt, 4).then(function(hash) { + passAdd = "&hash=" + hash; + createRoomCallback(passAdd, passAdd2); + }); } else { createRoomCallback(passAdd, passAdd2); } } -function hideDirectorinvites(ele){ - - if (getById("directorLinks2").style.display=="none"){ - ele.innerHTML=' LINKS (GUEST INVITES & SCENES)'; - getById("directorLinks2").style.display="inline-block"; +function hideDirectorinvites(ele) { + + if (getById("directorLinks2").style.display == "none") { + ele.innerHTML = ' LINKS (GUEST INVITES & SCENES)'; + getById("directorLinks2").style.display = "inline-block"; getById("customizeLinks").classList.remove("advanced"); } else { - ele.innerHTML=' LINKS (GUEST INVITES & SCENES)' - getById("directorLinks2").style.display="none"; - getById("help_directors_room").style.display="none"; - getById("roomnotes2").style.display="none"; + ele.innerHTML = ' LINKS (GUEST INVITES & SCENES)' + getById("directorLinks2").style.display = "none"; + getById("help_directors_room").style.display = "none"; + getById("roomnotes2").style.display = "none"; getById("customizeLinks").classList.add("advanced"); } - if (getById("directorLinks1").style.display=="none"){ - getById("directorLinks1").style.display="inline-block"; + if (getById("directorLinks1").style.display == "none") { + getById("directorLinks1").style.display = "inline-block"; getById("customizeLinks").classList.remove("advanced"); } else { - getById("directorLinks1").style.display="none"; - getById("help_directors_room").style.display="none"; - getById("roomnotes2").style.display="none"; + getById("directorLinks1").style.display = "none"; + getById("help_directors_room").style.display = "none"; + getById("roomnotes2").style.display = "none"; getById("customizeLinks").classList.add("advanced"); - + } //document.querySelector(".directorContainer.half").style.display="none"; //document.querySelector(".directorContainer").style.display="none"; } -function createRoomCallback(passAdd, passAdd2){ + +function createRoomCallback(passAdd, passAdd2) { var gridlayout = getById("gridlayout"); gridlayout.classList.add("directorsgrid"); var broadcastFlag = getById("broadcastFlag"); - try{ - if (broadcastFlag.checked){ - broadcastFlag=true; + try { + if (broadcastFlag.checked) { + broadcastFlag = true; } else { - broadcastFlag=false; + broadcastFlag = false; } - } catch (e){broadcastFlag=false;} - - var broadcastString = ""; - if (broadcastFlag){ - broadcastString = "&broadcast"; - getById("broadcastSlider").checked=true; + } catch (e) { + broadcastFlag = false; } + var broadcastString = ""; + if (broadcastFlag) { + broadcastString = "&broadcast"; + getById("broadcastSlider").checked = true; + } + + var showdirectorFlag = getById("showdirectorFlag"); + try { + if (showdirectorFlag.checked) { + showdirectorFlag = true; + } else { + showdirectorFlag = false; + } + } catch (e) { + showdirectorFlag = false; + } + + if (showdirectorFlag) { + updateURL("showdirector", true, false); + session.showDirector = true; + //getById("broadcastSlider").checked=true; + } + + var codecGroupFlag = getById("codecGroupFlag"); - - if (codecGroupFlag.value){ - if (codecGroupFlag.value==="vp9"){ + + if (codecGroupFlag.value) { + if (codecGroupFlag.value === "vp9") { codecGroupFlag = "&codec=vp9"; - } else if (codecGroupFlag.value==="h264"){ + } else if (codecGroupFlag.value === "h264") { codecGroupFlag = "&codec=h264"; - } else if (codecGroupFlag.value==="vp8"){ + } else if (codecGroupFlag.value === "vp8") { codecGroupFlag = "&codec=vp8"; - } else{ + } else { codecGroupFlag = ""; } } else { codecGroupFlag = ""; } - if (codecGroupFlag){ + if (codecGroupFlag) { session.codecGroupFlag = codecGroupFlag; } - + formSubmitting = false; @@ -4114,214 +4818,242 @@ function createRoomCallback(passAdd, passAdd2){ //getById("head3").className = 'advanced'; getById("head4").className = ''; - try{ - if (session.label===false){ - if (document.title==""){ + try { + if (session.label === false) { + if (document.title == "") { document.title = "Control Room"; } else { document.title += " - Control Room"; } } - } catch(e){errorlog(e);}; - - + } catch (e) { + errorlog(e); + }; + + session.director = true; + screensharesupport = false; + window.addEventListener("resize", updateMixer); window.addEventListener("orientationchange", updateMixer); getById("reshare").parentNode.removeChild(getById("reshare")); - - + + //getById("mutespeakerbutton").style.display = null; session.speakerMuted = true; // the director will start with audio playback muted. toggleSpeakerMute(true); - - - if (session.cleanDirector==false){ - + + + if (session.cleanDirector == false) { + getById("roomHeader").style.display = ""; //getById("directorLinks").style.display = ""; getById("directorLinks1").style.display = "inline-block"; getById("directorLinks2").style.display = "inline-block"; - - getById("director_block_1").dataset.raw = "https://"+location.host+location.pathname+"?room="+session.roomid+broadcastString+passAdd; - getById("director_block_1").href= "https://"+location.host+location.pathname+"?room="+session.roomid+broadcastString+passAdd; - getById("director_block_1").innerText= "https://"+location.host+location.pathname+"?room="+session.roomid+broadcastString+passAdd; - - - getById("director_block_3").dataset.raw = "https://"+location.host+location.pathname+"?scene&room="+session.roomid+codecGroupFlag+passAdd2; - getById("director_block_3").href= "https://"+location.host+location.pathname+"?scene&room="+session.roomid+codecGroupFlag+passAdd2; - getById("director_block_3").innerText= "https://"+location.host+location.pathname+"?scene&room="+session.roomid+codecGroupFlag+passAdd2; - - - + + getById("director_block_1").dataset.raw = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd; + getById("director_block_1").href = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd; + getById("director_block_1").innerText = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd; + + + getById("director_block_3").dataset.raw = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2; + getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2; + getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2; + + } else { getById("guestFeeds").innerHTML = ''; } getById("guestFeeds").style.display = ""; - - if (!(session.cleanOutput)){ + + if (!(session.cleanOutput)) { getById("chatbutton").classList.remove("advanced"); getById("controlButtons").style.display = "inherit"; getById("mutespeakerbutton").classList.remove("advanced"); - getById("miniPerformer").innerHTML = ''; + if (session.showDirector == false) { + getById("miniPerformer").innerHTML = ''; + } else { + getById("miniPerformer").innerHTML = ''; + } getById("miniPerformer").className = ""; } else { getById("miniPerformer").style.display = "none"; getById("controlButtons").style.display = "none"; } - + + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("advanced"); + getById("controlButtons").style.display = "inherit"; + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("advanced"); + } + joinRoom(session.roomid); } -function requestAudioSettings(ele){ + +function requestAudioSettings(ele) { var UUID = ele.dataset.UUID; - if (ele.dataset.active=="true"){ - ele.dataset.active="false"; + if (ele.dataset.active == "true") { + ele.dataset.active = "false"; ele.classList.remove("pressed"); - getById("advanced_audio_director_"+UUID).innerHTML = ""; - getById("advanced_audio_director_"+UUID).className = "advanced"; + getById("advanced_audio_director_" + UUID).innerHTML = ""; + getById("advanced_audio_director_" + UUID).className = "advanced"; } else { - ele.dataset.active="true"; + ele.dataset.active = "true"; ele.classList.add("pressed"); - getById("advanced_audio_director_"+UUID).innerHTML = ""; - var actionMsg = {}; + getById("advanced_audio_director_" + UUID).innerHTML = ""; + var actionMsg = {}; actionMsg.getAudioSettings = true; session.sendRequest(actionMsg, UUID); } } -function requestVideoSettings(ele){ + +function requestVideoSettings(ele) { var UUID = ele.dataset.UUID; - if (ele.dataset.active=="true"){ - ele.dataset.active="false"; + if (ele.dataset.active == "true") { + ele.dataset.active = "false"; ele.classList.remove("pressed"); - getById("advanced_video_director_"+UUID).innerHTML = ""; - getById("advanced_video_director_"+UUID).className = "advanced"; + getById("advanced_video_director_" + UUID).innerHTML = ""; + getById("advanced_video_director_" + UUID).className = "advanced"; } else { - ele.dataset.active="true"; + ele.dataset.active = "true"; ele.classList.add("pressed"); - getById("advanced_video_director_"+UUID).innerHTML = ""; - var actionMsg = {}; + getById("advanced_video_director_" + UUID).innerHTML = ""; + var actionMsg = {}; actionMsg.getVideoSettings = true; session.sendRequest(actionMsg, UUID); } } -function createControlBox(UUID, soloLink, streamID){ - if (document.getElementById("deleteme")){ + +function createControlBox(UUID, soloLink, streamID) { + if (document.getElementById("deleteme")) { getById("deleteme").parentNode.removeChild(getById("deleteme")); } var controls = getById("controls_blank").cloneNode(true); - - var container = document.createElement("div"); - container.id = "container_"+UUID; // needed to delete on user disconnect + + var container = document.createElement("div"); + container.id = "container_" + UUID; // needed to delete on user disconnect container.className = "vidcon"; - container.style.margin="15px 20px 0 0 "; - + container.style.margin = "15px 20px 0 0 "; + controls.style.display = "block"; - controls.id = "controls_"+UUID; - getById("guestFeeds").appendChild(container); - - var buttons = "
    ID: "+streamID+"\ - \ - \ + controls.id = "controls_" + UUID; + getById("guestFeeds").appendChild(container); + + var buttons = "
    ID: " + streamID + "\ + \ + \
    "; - if (!session.rpcs[UUID].voiceMeter){ + if (!session.rpcs[UUID].voiceMeter) { session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate").cloneNode(true); + session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID; session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary session.rpcs[UUID].voiceMeter.style.display = "block"; session.rpcs[UUID].voiceMeter.dataset.level = 0; } - + session.rpcs[UUID].voiceMeter.style.top = "1vh"; session.rpcs[UUID].voiceMeter.style.right = "1vh"; - - var videoContainer = document.createElement("div"); - videoContainer.id = "videoContainer_"+UUID; // needed to delete on user disconnect - videoContainer.style.margin="0"; + + + session.rpcs[UUID].remoteMuteElement = getById("muteStateTemplate").cloneNode(true); + session.rpcs[UUID].remoteMuteElement.id = ""; + session.rpcs[UUID].remoteMuteElement.style.top = "1vh"; + session.rpcs[UUID].remoteMuteElement.style.right = "1vh"; + + + var videoContainer = document.createElement("div"); + videoContainer.id = "videoContainer_" + UUID; // needed to delete on user disconnect + videoContainer.style.margin = "0"; videoContainer.style.position = "relative"; - - controls.innerHTML += "
    "; - controls.innerHTML += "
    "; - - var handsID = "hands_"+UUID; + + controls.innerHTML += "
    "; + controls.innerHTML += "
    "; + + var handsID = "hands_" + UUID; controls.innerHTML += "
    \
    \ "+soloLink+"\ + value='" + soloLink + "' href='" + soloLink + "'/>" + soloLink + "\ \
    \ - \
    "; - - controls.querySelectorAll('[data-action-type]').forEach((ele)=>{ // give action buttons some self-reference + + controls.querySelectorAll('[data-action-type]').forEach((ele) => { // give action buttons some self-reference ele.dataset.UUID = UUID; }); - + container.innerHTML = buttons; container.appendChild(videoContainer); videoContainer.appendChild(session.rpcs[UUID].voiceMeter); + videoContainer.appendChild(session.rpcs[UUID].remoteMuteElement); container.appendChild(controls); } -function createDirectorCam(vid, clean){ - vid.title = "This is the mini-preview of the Director's audio and video output"; - +function createDirectorCam(vid, clean) { + vid.title = "This is the mini-preview of the Director's audio and video output. The director cannot be muted by guest and does not show up in scenes."; + getById("press2talk").innerHTML = ""; getById("press2talk").outerHTML = ""; getById("miniPerformer").appendChild(vid); - getById("press2talk").dataset.enabled="true"; - session.muted=false; + getById("press2talk").dataset.enabled = "true"; + session.muted = false; toggleMute(true); getById("screensharebutton").classList.remove("advanced"); getById("hangupbutton2").classList.remove("advanced"); - setTimeout(function(){toggleSettings();},200); - - if (urlParams.has('permaid')){ - updateURL("permaid="+session.streamID); + setTimeout(function() { + toggleSettings(); + }, 200); + + if (urlParams.has('permaid')) { + updateURL("permaid=" + session.streamID); } else { - updateURL("push="+session.streamID); + updateURL("push=" + session.streamID); } - + } -function press2talk(clean=false){ +function press2talk(clean = false) { var ele = getById("press2talk"); - ele.style.minWidth="127px"; - ele.style.padding="7px"; + ele.style.minWidth = "127px"; + ele.style.padding = "7px"; getById("settingsbutton").classList.remove("advanced"); - + session.publishDirector(clean); - session.muted=false; + session.muted = false; toggleMute(true); - + } -function toggle(ele, tog=false, inline=true) { - var x = ele; - if (x.style.display === "none") { - if(inline){ - x.style.display = "inline-block"; +function toggle(ele, tog = false, inline = true) { + var x = ele; + if (x.style.display === "none") { + if (inline) { + x.style.display = "inline-block"; + } else { + x.style.display = "block"; + } } else { - x.style.display = "block"; + x.style.display = "none"; + } + if (tog) { + if (tog.dataset.saved) { + tog.innerHTML = tog.dataset.saved; + delete(tog.dataset.saved); + } else { + tog.dataset.saved = tog.innerHTML; + tog.innerHTML = "Hide This"; + } } - } else { - x.style.display = "none"; - } - if (tog){ - if (tog.dataset.saved){ - tog.innerHTML= tog.dataset.saved; - delete(tog.dataset.saved); - } else { - tog.dataset.saved = tog.innerHTML; - tog.innerHTML = "Hide This"; - } - } } @@ -4330,9 +5062,9 @@ var SelectedAudioInputDevices = []; // .. var SelectedVideoInputDevices = []; // .. function enumerateDevices() { - + log("enumerated start"); - + if (typeof navigator.enumerateDevices === "function") { log("enumerated failed 1"); return navigator.enumerateDevices(); @@ -4351,117 +5083,126 @@ function enumerateDevices() { }) .map(device => { return { - deviceId: device.deviceId != null ? device.deviceId : "", - groupId: device.groupId, - kind: "videoinput", - label: device.label, - toJSON: /* istanbul ignore next */ function () { + deviceId: device.deviceId != null ? device.deviceId : "" + , groupId: device.groupId + , kind: "videoinput" + , label: device.label + , toJSON: /* istanbul ignore next */ function() { return this; } }; })); }); - } - catch (e) { + } catch (e) { errorlog(e); } }); } } -function requestOutputAudioStream(){ +function requestOutputAudioStream() { try { //warnlog("GET USER MEDIA"); - return navigator.mediaDevices.getUserMedia({audio:true, video:false }).then(function(stream1){ // Apple needs thi to happen before I can access EnumerateDevices. + return navigator.mediaDevices.getUserMedia({ + audio: true + , video: false + }).then(function(stream1) { // Apple needs thi to happen before I can access EnumerateDevices. log("get media sources; request audio stream"); - return enumerateDevices().then(function(deviceInfos){ - stream1.getTracks().forEach(function(track) { // We don't want to keep it without audio; so we are going to try to add audio now. - track.stop(); // I need to do this after the enumeration step, else it breaks firefox's labels - }); - const audioOutputSelect = document.querySelector('#outputSourceScreenshare'); - audioOutputSelect.remove(0); - audioOutputSelect.removeAttribute("onclick"); - - for (let i = 0; i !== deviceInfos.length; ++i) { - const deviceInfo = deviceInfos[i]; - if (deviceInfo==null){continue;} - const option = document.createElement('option'); - option.value = deviceInfo.deviceId; - if (deviceInfo.kind === 'audiooutput') { - const option = document.createElement('option'); - if (audioOutputSelect.length===0){ - option.dataset.default = true; - } else { - option.dataset.default = false; - } - option.value = deviceInfo.deviceId || "default"; - if (option.value == session.sink){ - option.selected = true; - } - option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; - audioOutputSelect.appendChild(option); - } else { - log('Some other kind of source/device: ', deviceInfo); - } + return enumerateDevices().then(function(deviceInfos) { + stream1.getTracks().forEach(function(track) { // We don't want to keep it without audio; so we are going to try to add audio now. + track.stop(); // I need to do this after the enumeration step, else it breaks firefox's labels + }); + const audioOutputSelect = document.querySelector('#outputSourceScreenshare'); + audioOutputSelect.remove(0); + audioOutputSelect.removeAttribute("onclick"); + + for (let i = 0; i !== deviceInfos.length; ++i) { + const deviceInfo = deviceInfos[i]; + if (deviceInfo == null) { + continue; } - }); - }); - } catch (e){ - if (!(session.cleanOutput)){ - if (window.isSecureContext) { + const option = document.createElement('option'); + option.value = deviceInfo.deviceId; + if (deviceInfo.kind === 'audiooutput') { + const option = document.createElement('option'); + if (audioOutputSelect.length === 0) { + option.dataset.default = true; + } else { + option.dataset.default = false; + } + option.value = deviceInfo.deviceId || "default"; + if (option.value == session.sink) { + option.selected = true; + } + option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; + audioOutputSelect.appendChild(option); + } else { + log('Some other kind of source/device: ', deviceInfo); + } + } + }); + }); + } catch (e) { + if (!(session.cleanOutput)) { + if (window.isSecureContext) { alert("An error has occured when trying to access the default audio device. The reason is not known."); - } else if ((iOS) || (iPad)){ + } else if ((iOS) || (iPad)) { alert("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported."); - } else { + } else { alert("Error acessing the default audio device.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia"); - } - } - } + } + } + } } -function requestAudioStream(){ +function requestAudioStream() { try { //warnlog("GET USER MEDIA"); - return navigator.mediaDevices.getUserMedia({audio:true, video:false }).then(function(stream1){ // Apple needs thi to happen before I can access EnumerateDevices. + return navigator.mediaDevices.getUserMedia({ + audio: true + , video: false + }).then(function(stream1) { // Apple needs thi to happen before I can access EnumerateDevices. log("get media sources; request audio stream"); - return enumerateDevices().then(function(deviceInfos){ - stream1.getTracks().forEach(function(track) { // We don't want to keep it without audio; so we are going to try to add audio now. - track.stop(); // I need to do this after the enumeration step, else it breaks firefox's labels - }); - log("updating audio"); - const audioInputSelect = document.querySelector('select#audioSourceScreenshare'); - audioInputSelect.remove(1); - audioInputSelect.removeAttribute("onchange"); - - - for (let i = 0; i !== deviceInfos.length; ++i) { - const deviceInfo = deviceInfos[i]; - if (deviceInfo==null){continue;} - const option = document.createElement('option'); - option.value = deviceInfo.deviceId; - if (deviceInfo.kind === 'audioinput') { - option.text = deviceInfo.label || `Microphone ${audioInputSelect.length + 1}`; - audioInputSelect.appendChild(option); - } else { - log('Some other kind of source/device: ', deviceInfo); - } + return enumerateDevices().then(function(deviceInfos) { + stream1.getTracks().forEach(function(track) { // We don't want to keep it without audio; so we are going to try to add audio now. + track.stop(); // I need to do this after the enumeration step, else it breaks firefox's labels + }); + log("updating audio"); + const audioInputSelect = document.querySelector('select#audioSourceScreenshare'); + audioInputSelect.remove(1); + audioInputSelect.removeAttribute("onchange"); + + + for (let i = 0; i !== deviceInfos.length; ++i) { + const deviceInfo = deviceInfos[i]; + if (deviceInfo == null) { + continue; } - audioInputSelect.style.minHeight = ((audioInputSelect.childElementCount + 1)*1.15 * 16) + 'px'; - audioInputSelect.style.minWidth = "342px"; - }); - }); - } catch (e){ - if (!(session.cleanOutput)){ - if (window.isSecureContext) { + const option = document.createElement('option'); + option.value = deviceInfo.deviceId; + if (deviceInfo.kind === 'audioinput') { + option.text = deviceInfo.label || `Microphone ${audioInputSelect.length + 1}`; + audioInputSelect.appendChild(option); + } else { + log('Some other kind of source/device: ', deviceInfo); + } + } + audioInputSelect.style.minHeight = ((audioInputSelect.childElementCount + 1) * 1.15 * 16) + 'px'; + audioInputSelect.style.minWidth = "342px"; + }); + }); + } catch (e) { + if (!(session.cleanOutput)) { + if (window.isSecureContext) { alert("An error has occured when trying to access the default audio device. The reason is not known."); - } else if ((iOS) || (iPad)){ + } else if ((iOS) || (iPad)) { alert("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported."); - } else { + } else { alert("Error acessing the default audio device.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia"); - } - } - } + } + } + } } @@ -4469,57 +5210,62 @@ function gotDevices(deviceInfos) { // https://github.com/webrtc/samples/blob/gh- log("got devices!"); log(deviceInfos); - try{ + try { const audioInputSelect = document.querySelector('#audioSource'); - + audioInputSelect.innerHTML = ""; var option = document.createElement('input'); - option.type="checkbox"; + option.type = "checkbox"; option.value = "ZZZ"; option.name = "multiselect1"; option.id = "multiselect1"; option.style.display = "none"; option.checked = true; - - + + var label = document.createElement('label'); label.for = option.name; label.innerHTML = 'No Audio'; - + var listele = document.createElement('li'); listele.appendChild(option); listele.appendChild(label); audioInputSelect.appendChild(listele); - - - option.onchange = function(event){ // make sure to clear 'no audio option' if anything else is selected - if (!(getById("multiselect1").checked)){ - getById("multiselect1").checked= true; - - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1){ - } else { + + + option.onchange = function(event) { // make sure to clear 'no audio option' if anything else is selected + if (!(getById("multiselect1").checked)) { + getById("multiselect1").checked = true; + + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1) {} else { SelectedAudioInputDevices.push(event.currentTarget.value); } - + log("CHECKED 1"); } else { - $(this).parent().parent().find('input[type="checkbox"]').not('#multiselect1').prop('checked', false); // EVIL, but validated. - - while (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1){ - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(event.currentTarget.value),1); + + var list = document.querySelectorAll("#audioSource>li>input"); + for (var i = 0; i < list.length; i++) { + if (list[i].id !== "multiselect1") { + list[i].checked = false; + } + } + + while (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(event.currentTarget.value), 1); } } }; - + getById('multiselect-trigger').dataset.state = '0'; getById('multiselect-trigger').classList.add('closed'); - getById('multiselect-trigger').classList.remove('open'); + getById('multiselect-trigger').classList.remove('open'); getById('chevarrow1').classList.add('bottom'); - + const videoSelect = document.querySelector('select#videoSourceSelect'); const audioOutputSelect = document.querySelector('#outputSource'); - const selectors = [ videoSelect]; + const selectors = [videoSelect]; const values = selectors.map(select => select.value); selectors.forEach(select => { @@ -4527,187 +5273,193 @@ function gotDevices(deviceInfos) { // https://github.com/webrtc/samples/blob/gh- select.removeChild(select.firstChild); } }); - - + + function comp(a, b) { - if (a.kind === 'audioinput'){return 0;} - else if (a.kind === 'audiooutput'){return 0;} + if (a.kind === 'audioinput') { + return 0; + } else if (a.kind === 'audiooutput') { + return 0; + } const labelA = a.label.toUpperCase(); const labelB = b.label.toUpperCase(); - if (labelA > labelB) { return 1;} - else if (labelA < labelB) { return -1; } + if (labelA > labelB) { + return 1; + } else if (labelA < labelB) { + return -1; + } return 0; } //deviceInfos.sort(comp); // I like this idea, but it messes with the defaults. I just don't know what it will do. - + // This is to hide NDI from default device. NDI Tools fucks up. var tmp = []; for (let i = 0; i !== deviceInfos.length; ++i) { deviceInfo = deviceInfos[i]; - if (!((deviceInfo.kind === 'videoinput') && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek")))){ + if (!((deviceInfo.kind === 'videoinput') && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek")))) { tmp.push(deviceInfo); } } - + for (let i = 0; i !== deviceInfos.length; ++i) { deviceInfo = deviceInfos[i]; - if ((deviceInfo.kind === 'videoinput') && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek"))){ + if ((deviceInfo.kind === 'videoinput') && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek"))) { tmp.push(deviceInfo); - log("V DEVICE FOUND = "+deviceInfo.label.replace(/[\W]+/g,"_").toLowerCase()); + log("V DEVICE FOUND = " + deviceInfo.label.replace(/[\W]+/g, "_").toLowerCase()); } } deviceInfos = tmp; log(deviceInfos); - - if ((session.audioDevice) && (session.audioDevice!==1)){ // this sorts according to users's manual selection + + if ((session.audioDevice) && (session.audioDevice !== 1)) { // this sorts according to users's manual selection var tmp = []; for (let i = 0; i !== deviceInfos.length; ++i) { deviceInfo = deviceInfos[i]; - if ((deviceInfo.kind === 'audioinput') && (deviceInfo.label.replace(/[\W]+/g,"_").toLowerCase().includes(session.audioDevice))){ + if ((deviceInfo.kind === 'audioinput') && (deviceInfo.label.replace(/[\W]+/g, "_").toLowerCase().includes(session.audioDevice))) { tmp.push(deviceInfo); - log("A DEVICE FOUND = "+deviceInfo.label.replace(/[\W]+/g,"_").toLowerCase()); + log("A DEVICE FOUND = " + deviceInfo.label.replace(/[\W]+/g, "_").toLowerCase()); } } for (let i = 0; i !== deviceInfos.length; ++i) { deviceInfo = deviceInfos[i]; - if (!((deviceInfo.kind === 'audioinput') && (deviceInfo.label.replace(/[\W]+/g,"_").toLowerCase().includes(session.audioDevice)))){ + if (!((deviceInfo.kind === 'audioinput') && (deviceInfo.label.replace(/[\W]+/g, "_").toLowerCase().includes(session.audioDevice)))) { tmp.push(deviceInfo); } } - + deviceInfos = tmp; log(session.audioDevice); log(deviceInfos); } - - - if ((session.videoDevice) && (session.videoDevice!==1)){ // this sorts according to users's manual selection + + + if ((session.videoDevice) && (session.videoDevice !== 1)) { // this sorts according to users's manual selection var tmp = []; for (let i = 0; i !== deviceInfos.length; ++i) { deviceInfo = deviceInfos[i]; - if ((deviceInfo.kind === 'videoinput') && (deviceInfo.label.replace(/[\W]+/g,"_").toLowerCase().includes(session.videoDevice))){ + if ((deviceInfo.kind === 'videoinput') && (deviceInfo.label.replace(/[\W]+/g, "_").toLowerCase().includes(session.videoDevice))) { tmp.push(deviceInfo); - log("V DEVICE FOUND = "+deviceInfo.label.replace(/[\W]+/g,"_").toLowerCase()); + log("V DEVICE FOUND = " + deviceInfo.label.replace(/[\W]+/g, "_").toLowerCase()); } } for (let i = 0; i !== deviceInfos.length; ++i) { deviceInfo = deviceInfos[i]; - if (!((deviceInfo.kind === 'videoinput') && (deviceInfo.label.replace(/[\W]+/g,"_").toLowerCase().includes(session.videoDevice)))){ + if (!((deviceInfo.kind === 'videoinput') && (deviceInfo.label.replace(/[\W]+/g, "_").toLowerCase().includes(session.videoDevice)))) { tmp.push(deviceInfo); } } deviceInfos = tmp; - log("VDECICE:"+session.videoDevice); + log("VDECICE:" + session.videoDevice); log(deviceInfos); } - - + + var counter = 1; for (let i = 0; i !== deviceInfos.length; ++i) { const deviceInfo = deviceInfos[i]; - if (deviceInfo==null){continue;} - + if (deviceInfo == null) { + continue; + } + if (deviceInfo.kind === 'audioinput') { option = document.createElement('input'); - option.type="checkbox"; + option.type = "checkbox"; counter++; listele = document.createElement('li'); - if (counter==2){ - option.checked=true; - listele.style.display="block"; - option.style.display="none"; + if (counter == 2) { + option.checked = true; + listele.style.display = "block"; + option.style.display = "none"; getById("multiselect1").checked = false; - getById("multiselect1").parentNode.style.display="none"; + getById("multiselect1").parentNode.style.display = "none"; } else { - listele.style.display="none"; + listele.style.display = "none"; } - - + + option.value = deviceInfo.deviceId || "default"; - option.name = "multiselect"+counter; - option.id = "multiselect"+counter; + option.name = "multiselect" + counter; + option.id = "multiselect" + counter; label = document.createElement('label'); label.for = option.name; - - label.innerHTML = " " + (deviceInfo.label || ("microphone "+ ((audioInputSelect.length || 0)+1))); - + + label.innerHTML = " " + (deviceInfo.label || ("microphone " + ((audioInputSelect.length || 0) + 1))); + listele.appendChild(option); listele.appendChild(label); audioInputSelect.appendChild(listele); - - option.onchange = function(event){ // make sure to clear 'no audio option' if anything else is selected + + option.onchange = function(event) { // make sure to clear 'no audio option' if anything else is selected getById("multiselect1").checked = false; log("UNCHECKED"); - if (!(CtrlPressed)){ + if (!(CtrlPressed)) { document.querySelectorAll("#audioSource input[type='checkbox']").forEach(function(item) { - if (event.currentTarget.id !== item.id){ + if (event.currentTarget.id !== item.id) { item.checked = false; - - while (SelectedAudioInputDevices.indexOf(item.value) > -1){ - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value),1); + + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); } - - } else { - item.checked = true; - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1){ - } else { + + } else { + item.checked = true; + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1) {} else { SelectedAudioInputDevices.push(event.currentTarget.value); } - } + } }); } }; - + } else if (deviceInfo.kind === 'videoinput') { option = document.createElement('option'); option.value = deviceInfo.deviceId || "default"; option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; videoSelect.appendChild(option); - } else if (deviceInfo.kind === 'audiooutput'){ + } else if (deviceInfo.kind === 'audiooutput') { option = document.createElement('option'); - if (audioOutputSelect.length===0){ + if (audioOutputSelect.length === 0) { option.dataset.default = true; } else { option.dataset.default = false; } option.value = deviceInfo.deviceId || "default"; - if (option.value == session.sink){ + if (option.value == session.sink) { option.selected = true; - } + } option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; audioOutputSelect.appendChild(option); } else { log('Some other kind of source/device: ', deviceInfo); } } - - if (audioOutputSelect.childNodes.length==0){ + + if (audioOutputSelect.childNodes.length == 0) { option = document.createElement('option'); option.value = "default"; option.text = "System Default"; audioOutputSelect.appendChild(option); } - - + option = document.createElement('option'); option.text = "Disable Video"; option.value = "ZZZ"; videoSelect.appendChild(option); // NO AUDIO OPTION - + selectors.forEach((select, selectorIndex) => { if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) { select.value = values[selectorIndex]; } }); - - } catch (e){ + + } catch (e) { errorlog(e); } } if (location.protocol !== 'https:') { - if (!(session.cleanOutput)){ + if (!(session.cleanOutput)) { alert("SSL (https) is not enabled. This site will not work without it!"); } } @@ -4718,168 +5470,257 @@ function getUserMediaVideoParams(resolutionFallbackLevel, isSafariBrowser) { case 0: if (isSafariBrowser) { return { - width: { min: 360, ideal: 1920, max: 1920 }, - height: { min: 360, ideal: 1080, max: 1080 } - }; + width: { + min: 360 + , ideal: 1920 + , max: 1920 + } + , height: { + min: 360 + , ideal: 1080 + , max: 1080 + } + }; } else { return { - width: { min: 720, ideal: 1920, max: 1920 }, - height: { min: 720, ideal: 1080, max: 1920 } + width: { + min: 720 + , ideal: 1920 + , max: 1920 + } + , height: { + min: 720 + , ideal: 1080 + , max: 1920 + } }; } case 1: if (isSafariBrowser) { return { - width: { min: 360, ideal: 1280, max: 1280 }, - height: { min: 360, ideal: 720, max: 720 } - }; + width: { + min: 360 + , ideal: 1280 + , max: 1280 + } + , height: { + min: 360 + , ideal: 720 + , max: 720 + } + }; } else { return { - width: { min: 720, ideal: 1280, max: 1280 }, - height: { min: 720, ideal: 720, max: 1280 } + width: { + min: 720 + , ideal: 1280 + , max: 1280 + } + , height: { + min: 720 + , ideal: 720 + , max: 1280 + } }; } case 2: if (isSafariBrowser) { return { - width: { min: 640 }, - height: { min: 360 } + width: { + min: 640 + } + , height: { + min: 360 + } }; } else { return { - width: { min: 240, ideal: 640, max: 1280 }, - height: { min: 240, ideal: 360, max: 1280 } + width: { + min: 240 + , ideal: 640 + , max: 1280 + } + , height: { + min: 240 + , ideal: 360 + , max: 1280 + } }; } case 3: if (isSafariBrowser) { return { - width: { min: 360, ideal: 1280, max: 1440 } + width: { + min: 360 + , ideal: 1280 + , max: 1440 + } }; - } - else { + } else { return { - width: { min: 360, ideal: 1280, max: 1440 } + width: { + min: 360 + , ideal: 1280 + , max: 1440 + } }; } case 4: if (isSafariBrowser) { return { - height: { min: 360, ideal: 720, max: 960 } + height: { + min: 360 + , ideal: 720 + , max: 960 + } }; - } - else { + } else { return { - height: { ideal: 720, max: 960 } + height: { + ideal: 720 + , max: 960 + } }; } case 5: if (isSafariBrowser) { return { - width: { min: 360, ideal: 640, max: 1440 }, - height: { min: 360, ideal: 360, max: 720 } + width: { + min: 360 + , ideal: 640 + , max: 1440 + } + , height: { + min: 360 + , ideal: 360 + , max: 720 + } }; - } - else { + } else { return { - width: { ideal:640, max:1920}, - height: { ideal: 360, max:1920}}; // same as default, but I didn't want to mess with framerates until I gave it all a try first + width: { + ideal: 640 + , max: 1920 + } + , height: { + ideal: 360 + , max: 1920 + } + }; // same as default, but I didn't want to mess with framerates until I gave it all a try first } case 6: if (isSafariBrowser) { return {}; // iphone users probably don't need to wait any longer, so let them just get to it - } - else { + } else { return { - width: { min: 360, ideal: 640, max: 3840 }, - height: { min: 360, ideal: 360, max: 2160 } + width: { + min: 360 + , ideal: 640 + , max: 3840 + } + , height: { + min: 360 + , ideal: 360 + , max: 2160 + } }; - + } case 7: return { // If the camera is recording in low-light, it may have a low framerate. It coudl also be recording at a very high resolution. - width: { min: 360, ideal: 640 }, - height: { min: 360, ideal: 360 }, - }; + width: { + min: 360 + , ideal: 640 + } + , height: { + min: 360 + , ideal: 360 + } + , }; case 8: return { - width: {min:360}, - height: {min:360}, - frameRate: 10 - }; // same as default, but I didn't want to mess with framerates until I gave it all a try first + width: { + min: 360 + } + , height: { + min: 360 + } + , frameRate: 10 + }; // same as default, but I didn't want to mess with framerates until I gave it all a try first case 9: - return {frameRate: 0 }; // Some Samsung Devices report they can only support a framerate of 0. + return { + frameRate: 0 + }; // Some Samsung Devices report they can only support a framerate of 0. case 10: return {} default: - return {}; + return {}; } } -function addScreenDevices(device){ - if (device.kind=="audio"){ +function addScreenDevices(device) { + if (device.kind == "audio") { const audioInputSelect = document.querySelector('#audioSource3'); const listele = document.createElement('li'); - listele.style.display="block"; - + listele.style.display = "block"; + const option = document.createElement('input'); - option.type="checkbox"; - option.checked = true; - - if (getById('multiselect-trigger3').dataset.state==0){ + option.type = "checkbox"; + option.checked = true; + + if (getById('multiselect-trigger3').dataset.state == 0) { option.style.display = "none"; } - + option.value = device.id; option.name = device.label; option.dataset.type = "screen"; option.label = device.label; - + const label = document.createElement('label'); label.for = option.name; - label.innerHTML = " "+device.label; + label.innerHTML = " " + device.label; listele.appendChild(option); listele.appendChild(label); - - option.onchange = function(event){ // make sure to clear 'no audio option' if anything else is selected + + option.onchange = function(event) { // make sure to clear 'no audio option' if anything else is selected log("change 4644"); - if (!(CtrlPressed)){ + if (!(CtrlPressed)) { document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function(item) { - if (event.currentTarget.value !== item.value){ // this shoulnd't happen, but if it does. - - item.checked = false; - - if (item.dataset.type == "screen"){ - item.parentElement.parentElement.removeChild(item.parentElement); - } - - while (SelectedAudioInputDevices.indexOf(item.value) > -1){ - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value),1); - } - - activatedPreview=false; - grabAudio("videosource","#audioSource3"); // exclude item.id - - } else { - if (SelectedAudioInputDevices.indexOf(item.value) > -1){ + if (event.currentTarget.value !== item.value) { // this shoulnd't happen, but if it does. + + item.checked = false; + + if (item.dataset.type == "screen") { + item.parentElement.parentElement.removeChild(item.parentElement); + } + + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); + } + + activatedPreview = false; + grabAudio("videosource", "#audioSource3"); // exclude item.id + } else { - SelectedAudioInputDevices.push(item.value); + if (SelectedAudioInputDevices.indexOf(item.value) > -1) {} else { + SelectedAudioInputDevices.push(item.value); + } + + item.checked = true; + activatedPreview = false; + grabAudio("videosource", "#audioSource3", item.value); // exclude item.id. we will reconnect, even if already connected, as a way to 'reset' a device if it isn't working. } - - item.checked = true; - activatedPreview=false; - grabAudio("videosource","#audioSource3",item.value); // exclude item.id. we will reconnect, even if already connected, as a way to 'reset' a device if it isn't working. - } }); } event.stopPropagation(); return false; }; audioInputSelect.appendChild(listele); - getById("audioSourceNoAudio2").checked=false; - - } else if (device.kind=="video"){ + getById("audioSourceNoAudio2").checked = false; + + } else if (device.kind == "video") { const videoSelect = document.querySelector('select#videoSource3'); //const selectors = [ videoSelect]; //const values = selectors.map(select => select.value); @@ -4892,205 +5733,211 @@ function addScreenDevices(device){ } } -function gotDevices2(deviceInfos){ +function gotDevices2(deviceInfos) { log("got devices!"); log(deviceInfos); - - getById("multiselect-trigger3").dataset.state="0"; + + getById("multiselect-trigger3").dataset.state = "0"; getById("multiselect-trigger3").classList.add('closed'); getById("multiselect-trigger3").classList.remove('open'); getById("chevarrow2").classList.add('bottom'); - + var knownTrack = false; - - try{ + + try { const audioInputSelect = document.querySelector('#audioSource3'); const videoSelect = document.querySelector('select#videoSource3'); const audioOutputSelect = document.querySelector('#outputSource3'); - const selectors = [ videoSelect]; - + const selectors = [videoSelect]; + [audioInputSelect].forEach(select => { while (select.firstChild) { select.removeChild(select.firstChild); } }); - + const values = selectors.map(select => select.value); selectors.forEach(select => { while (select.firstChild) { select.removeChild(select.firstChild); } }); - + [audioOutputSelect].forEach(select => { while (select.firstChild) { select.removeChild(select.firstChild); } }); - + var counter = 0; - for (let i = 0; i !== deviceInfos.length; ++i){ + for (let i = 0; i !== deviceInfos.length; ++i) { const deviceInfo = deviceInfos[i]; - if (deviceInfo==null){continue;} - + if (deviceInfo == null) { + continue; + } + if (deviceInfo.kind === 'audioinput') { const option = document.createElement('input'); - option.type="checkbox"; + option.type = "checkbox"; counter++; const listele = document.createElement('li'); - listele.style.display="none"; - + listele.style.display = "none"; + try { - session.streamSrc.getAudioTracks().forEach(function(track){ - if (deviceInfo.label==track.label){ + session.streamSrc.getAudioTracks().forEach(function(track) { + if (deviceInfo.label == track.label) { option.checked = true; - listele.style.display="inherit"; - } + listele.style.display = "inherit"; + } }); - } catch(e){errorlog(e);} - + } catch (e) { + errorlog(e); + } + option.style.display = "none" option.value = deviceInfo.deviceId || "default"; - option.name = "multiselecta"+counter; - option.id = "multiselecta"+counter; - option.dataset.label = deviceInfo.label || ("microphone "+ ((audioInputSelect.length || 0)+1)); - + option.name = "multiselecta" + counter; + option.id = "multiselecta" + counter; + option.dataset.label = deviceInfo.label || ("microphone " + ((audioInputSelect.length || 0) + 1)); + const label = document.createElement('label'); label.for = option.name; - - label.innerHTML = " " + (deviceInfo.label || ("microphone "+ ((audioInputSelect.length || 0)+1))); - + + label.innerHTML = " " + (deviceInfo.label || ("microphone " + ((audioInputSelect.length || 0) + 1))); + listele.appendChild(option); listele.appendChild(label); audioInputSelect.appendChild(listele); - - option.onchange = function(event){ // make sure to clear 'no audio option' if anything else is selected + + option.onchange = function(event) { // make sure to clear 'no audio option' if anything else is selected log("change 4768"); - if (!(CtrlPressed)){ + if (!(CtrlPressed)) { document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function(item) { - if (event.currentTarget.value !== item.value){ + if (event.currentTarget.value !== item.value) { item.checked = false; - if (item.dataset.type == "screen"){ + if (item.dataset.type == "screen") { item.parentElement.parentElement.removeChild(item.parentElement); - } - while (SelectedAudioInputDevices.indexOf(item.value) > -1){ - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value),1); + } + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); } } else { item.checked = true; - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1){ - } else { + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1) {} else { SelectedAudioInputDevices.push(event.currentTarget.value); } } }); } else { - - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1){ - } else { + + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1) {} else { SelectedAudioInputDevices.push(event.currentTarget.value); } - - getById("audioSourceNoAudio2").checked=false; + + getById("audioSourceNoAudio2").checked = false; } }; - - } else if (deviceInfo.kind === 'videoinput'){ + + } else if (deviceInfo.kind === 'videoinput') { const option = document.createElement('option'); option.value = deviceInfo.deviceId || "default"; option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; try { - session.streamSrc.getVideoTracks().forEach(function(track){ - if (option.text==track.label){ + session.streamSrc.getVideoTracks().forEach(function(track) { + if (option.text == track.label) { option.selected = true; - knownTrack=true; + knownTrack = true; } }); - } catch(e){errorlog(e);} + } catch (e) { + errorlog(e); + } videoSelect.appendChild(option); - - } else if (deviceInfo.kind === 'audiooutput'){ + + } else if (deviceInfo.kind === 'audiooutput') { const option = document.createElement('option'); - if (audioOutputSelect.length===0){ + if (audioOutputSelect.length === 0) { option.dataset.default = true; } else { option.dataset.default = false; } option.value = deviceInfo.deviceId || "default"; - if (option.value == session.sink){ + if (option.value == session.sink) { option.selected = true; } option.text = deviceInfo.label || `Speaker ${outputSelect.length + 1}`; audioOutputSelect.appendChild(option); - + } else { log('Some other kind of source/device: ', deviceInfo); } } - - if (audioOutputSelect.childNodes.length==0){ + + if (audioOutputSelect.childNodes.length == 0) { const option = document.createElement('option'); option.value = "default"; option.text = "System Default"; audioOutputSelect.appendChild(option); } - + //////////// - - session.streamSrc.getAudioTracks().forEach(function(track){ // add active ScreenShare audio tracks to the list + + session.streamSrc.getAudioTracks().forEach(function(track) { // add active ScreenShare audio tracks to the list log("Checking for screenshare audio"); - var matched=false; - for (var i = 0; i !== deviceInfos.length; ++i){ + var matched = false; + for (var i = 0; i !== deviceInfos.length; ++i) { var deviceInfo = deviceInfos[i]; - if (deviceInfo==null){continue;} + if (deviceInfo == null) { + continue; + } log("---"); - if (track.label == deviceInfo.label){ - matched=true; + if (track.label == deviceInfo.label) { + matched = true; continue; } } - if (matched==false){ // Not a gUM device - + if (matched == false) { // Not a gUM device + var listele = document.createElement('li'); - listele.style.display="block"; + listele.style.display = "block"; var option = document.createElement('input'); - option.type="checkbox"; + option.type = "checkbox"; option.value = track.id; option.checked = true; option.style.display = "none"; option.name = track.label; option.label = track.label; - option.dataset.type="screen"; + option.dataset.type = "screen"; const label = document.createElement('label'); label.for = option.name; - label.innerHTML = " "+track.label; + label.innerHTML = " " + track.label; listele.appendChild(option); listele.appendChild(label); - option.onchange = function(event){ // make sure to clear 'no audio option' if anything else is selected + option.onchange = function(event) { // make sure to clear 'no audio option' if anything else is selected log("change 4873"); - var trackid=null; - if (!(CtrlPressed)){ - + var trackid = null; + if (!(CtrlPressed)) { + document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function(item) { - if (event.currentTarget.value !== item.value){ // this shoulnd't happen, but if it does. - item.checked = false; - if (item.dataset.type == "screen"){ - item.parentElement.parentElement.removeChild(item.parentElement); + if (event.currentTarget.value !== item.value) { // this shoulnd't happen, but if it does. + item.checked = false; + if (item.dataset.type == "screen") { + item.parentElement.parentElement.removeChild(item.parentElement); + } + } else { + event.currentTarget.checked = true; + trackid = item.value; } - } else { - event.currentTarget.checked = true; - trackid=item.value; - } }); } else { //getById("audioSourceNoAudio2").checked=false; - if (event.currentTarget.dataset.type == "screen"){ + if (event.currentTarget.dataset.type == "screen") { event.currentTarget.parentElement.parentElement.removeChild(event.currentTarget.parentElement); } } - activatedPreview=false; - grabAudio("videosource","#audioSource3",trackid); // exclude item.id. + activatedPreview = false; + grabAudio("videosource", "#audioSource3", trackid); // exclude item.id. event.stopPropagation(); return false; }; @@ -5098,403 +5945,466 @@ function gotDevices2(deviceInfos){ } }); /////////// no video option - + var optionss = false; + if (screensharesupport) { + optionss = document.createElement('option'); + optionss.text = "New Screen Share"; + optionss.value = "XXX"; + optionss.previous = + videoSelect.appendChild(optionss); // NO AUDIO OPTION + } + option = document.createElement('option'); // no video option.text = "Disable Video"; option.value = "ZZZ"; - videoSelect.appendChild(option); - if (session.streamSrc.getVideoTracks().length==0){ + videoSelect.appendChild(option); + if (session.streamSrc.getVideoTracks().length == 0) { option.selected = true; - } else if (knownTrack==false){ + } else if (knownTrack == false) { option = document.createElement('option'); // no video option.text = session.streamSrc.getVideoTracks()[0].label; option.value = "YYY"; - videoSelect.appendChild(option); + videoSelect.appendChild(option); option.selected = true; - + } - - - + + if (optionss) { + optionss.lastSelected = videoSelect.selectedIndex; + } + ///////////// /// NO AUDIO appended option - - var option = document.createElement('input'); - option.type="checkbox"; + + var option = document.createElement('input'); + option.type = "checkbox"; option.value = "ZZZ"; option.style.display = "none" option.id = "audioSourceNoAudio2"; - + var label = document.createElement('label'); label.for = option.name; label.innerHTML = " No Audio"; var listele = document.createElement('li'); - - if (session.streamSrc.getAudioTracks().length==0){ + + if (session.streamSrc.getAudioTracks().length == 0) { option.checked = true; } else { - listele.style.display="none"; + listele.style.display = "none"; option.checked = false; - } - option.onchange = function(event){ // make sure to clear 'no audio option' if anything else is selected + } + option.onchange = function(event) { // make sure to clear 'no audio option' if anything else is selected log("change 4938"); - if (!(CtrlPressed)){ + if (!(CtrlPressed)) { document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function(item) { - if (event.currentTarget.value !== item.value){ + if (event.currentTarget.value !== item.value) { item.checked = false; - if (item.dataset.type == "screen"){ + if (item.dataset.type == "screen") { item.parentElement.parentElement.removeChild(item.parentElement); } - - while (SelectedAudioInputDevices.indexOf(item.value) > -1){ - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value),1); + + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); } } else { item.checked = true; - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1){ - } else { + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1) {} else { SelectedAudioInputDevices.push(event.currentTarget.value); } } }); } else { document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function(item) { - if (event.currentTarget.value === item.value){ + if (event.currentTarget.value === item.value) { event.currentTarget.checked = true; - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1){ - } else { + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) > -1) {} else { SelectedAudioInputDevices.push(event.currentTarget.value); } } else { item.checked = false; - if (item.dataset.type == "screen"){ + if (item.dataset.type == "screen") { item.parentElement.parentElement.removeChild(item.parentElement); } - while (SelectedAudioInputDevices.indexOf(item.value) > -1){ - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value),1); + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); } } - + }); } - }; + }; listele.appendChild(option); listele.appendChild(label); audioInputSelect.appendChild(listele); - + //////////// - - + + selectors.forEach((select, selectorIndex) => { if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) { select.value = values[selectorIndex]; } }); - - audioInputSelect.onchange = function(){ + + audioInputSelect.onchange = function() { log("Audio OPTION HAS CHANGED? 2"); - activatedPreview=false; - grabAudio("videosource","#audioSource3"); + activatedPreview = false; + grabAudio("videosource", "#audioSource3"); }; - videoSelect.onchange = function(event){ + videoSelect.onchange = function(event) { + try { + if (event.target.options[event.target.options.selectedIndex].value === "XXX") { + videoSelect.selectedIndex = event.target.options[event.target.options.selectedIndex].lastSelected; + if (ScreenShareState == false) { + toggleScreenShare(); + } else { + toggleScreenShare(true); + } + return; + } + } catch (e) {} log("video source changed"); - activatedPreview=false; + activatedPreview = false; grabVideo(session.quality, "videosource", "select#videoSource3"); - enumerateDevices().then(gotDevices2).then(function(){ScreeShareState=false;}); + enumerateDevices().then(gotDevices2).then(function() { + ScreenShareState = false; + pokeIframeAPI("screen-share-ended"); + + getById("screensharebutton").classList.add("float"); + getById("screensharebutton").classList.remove("float2"); + }); }; - getById("refreshVideoButton").onclick = function(){ - if (ScreeShareState){log("can't refresh a screenshare");return;} - log("video source changed"); - activatedPreview=false; - grabVideo(session.quality, "videosource", "select#videoSource3"); - }; - - audioOutputSelect.onchange = function(){ - - if ((iOS) || (iPad)){ + getById("refreshVideoButton").onclick = function() { + if (ScreenShareState) { + log("can't refresh a screenshare"); return; } - + log("video source changed"); + activatedPreview = false; + grabVideo(session.quality, "videosource", "select#videoSource3"); + }; + + audioOutputSelect.onchange = function() { + + if ((iOS) || (iPad)) { + return; + } + var outputSelect = document.querySelector('select#outputSource3'); session.sink = outputSelect.options[outputSelect.selectedIndex].value; //if (session.sink=="default"){session.sink=false;} else { - try{ + try { getById("videosource").setSinkId(session.sink).then(() => { - log("New Output Device:"+session.sink); + log("New Output Device:" + session.sink); }).catch(error => { errorlog(error); }); - } catch(e){errorlog(e);} - for (UUID in session.rpcs){ + } catch (e) { + errorlog(e); + } + for (UUID in session.rpcs) { session.rpcs[UUID].videoElement.setSinkId(session.sink).then(() => { - log("New Output Device for: "+UUID); + log("New Output Device for: " + UUID); }).catch(error => { errorlog(error); }); } } - - } catch (e){ + + } catch (e) { errorlog(e); } } -function playtone(screen=false){ - - if ((iOS) || (iPad)){ - // try{ - // session.audioContext.resume(); - // } catch(e){errorlog(e);} +function playtone(screen = false) { + + if ((iOS) || (iPad)) { + // try{ + // session.audioContext.resume(); + // } catch(e){errorlog(e);} var testtone = document.getElementById("testtone"); - if (testtone){ + if (testtone) { testtone.play(); } return; } - - if (screen){ + + if (screen) { var outputSelect = document.querySelector('select#outputSourceScreenshare'); session.sink = outputSelect.options[outputSelect.selectedIndex].value; } - + var testtone = document.getElementById("testtone"); - if (testtone){ - if (session.sink){ - try{ - testtone.setSinkId(session.sink).then(() => { // TODO: iOS doens't support sink. Needs to bypass if IOS - log("changing audio sink:"+session.sink); + if (testtone) { + if (session.sink) { + try { + testtone.setSinkId(session.sink).then(() => { // TODO: iOS doens't support sink. Needs to bypass if IOS + log("changing audio sink:" + session.sink); testtone.play(); }).catch(error => { errolog("couldn't set sink"); errorlog(error); }); - } catch(e){errorlog(e);} + } catch (e) { + warnlog(e); // firefox? + testtone.play(); + } } else { testtone.play(); } } } -async function getAudioOnly(selector, trackid=null, override=false){ - var audioSelect = document.querySelector(selector).querySelectorAll("input"); +async function getAudioOnly(selector, trackid = null, override = false) { + var audioSelect = document.querySelector(selector).querySelectorAll("input"); var audioList = []; var streams = []; log("getAudioOnly()"); - for (var i=0; i { - }).catch(error => { + getById("previewWebcam").setSinkId(session.sink).then(() => {}).catch(error => { errorlog("4960"); errorlog(error); }); - } catch(e){errorlog(e);} + } catch (e) { + errorlog(e); + } return; } - - - if (session.streamSrc===null){return;} - if (document.getElementById("videosource")===null){return;} - - try{ - session.streamSrc.getTracks().forEach(function(track){ - - if (track.readyState == "ended"){ - if (track.kind=="audio"){ + + + if (session.streamSrc === null) { + return; + } + if (document.getElementById("videosource") === null) { + return; + } + + try { + session.streamSrc.getTracks().forEach(function(track) { + + if (track.readyState == "ended") { + if (track.kind == "audio") { lastAudioDevice = track.label; - } else if (track.kind=="video"){ + } else if (track.kind == "video") { lastVideoDevice = track.label; } session.streamSrc.removeTrack(track); log("remove ended old track"); } }); - - session.videoElement.srcObject.getTracks().forEach(function(track){ - if (track.readyState == "ended"){ + + session.videoElement.srcObject.getTracks().forEach(function(track) { + if (track.readyState == "ended") { session.videoElement.srcObject.removeTrack(track); log("remove ended old track"); } }); - - } catch (e){ + + } catch (e) { errorlog(e); } - + clearTimeout(audioReconnectTimeout); - audioReconnectTimeout=null; - if (lastAudioDevice){ - audioReconnectTimeout = setTimeout(function(){ // only reconnect same audio device. If reconnected, clear the disconnected flag. - enumerateDevices().then(gotDevices2).then(function(){ + audioReconnectTimeout = null; + if (lastAudioDevice) { + audioReconnectTimeout = setTimeout(function() { // only reconnect same audio device. If reconnected, clear the disconnected flag. + enumerateDevices().then(gotDevices2).then(function() { // TODO: check to see if any audio is connected? - var streamConnected=false; - var audioSelect = document.querySelector("#audioSource3").querySelectorAll("input"); - for (var i=0; i check if session.sink still exists -> if not, select default default (track past last sink) -> if last disconnected devices comes back, reconnect it. - + // lastPlaybackDevice //if (session.sink){ // Let Chrome handle the audio automatically, since not manually specified. clearTimeout(playbackReconnectTimeout); - playbackReconnectTimeout = setTimeout(function(){ - enumerateDevices().then(gotDevices2).then(function(){ + playbackReconnectTimeout = setTimeout(function() { + enumerateDevices().then(gotDevices2).then(function() { resetupAudioOut(); }); - },500); - + }, 500); + } -function resetupAudioOut(){ - if ((iOS) || (iPad)){ - for (var UUID in session.rpcs){ - if (session.rpcs[UUID].videoElement){ +function resetupAudioOut() { + if ((iOS) || (iPad)) { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement) { session.rpcs[UUID].videoElement.pause().then(() => { - setTimeout(function(uuid){ - session.rpcs[uuid].videoElement.play().then(() => {log("toggle pause/play");}); - },0, UUID) - + setTimeout(function(uuid) { + session.rpcs[uuid].videoElement.play().then(() => { + log("toggle pause/play"); + }); + }, 0, UUID) + }); } } return; } - - var outputSelect = document.getElementById("outputSource3"); - if (!outputSelect){errorlog("resetup audio failed");return;} + + var outputSelect = document.getElementById("outputSource3"); + if (!outputSelect) { + errorlog("resetup audio failed"); + return; + } log("Resetting Audio Output"); var sinkSet = false; - for (var i =0; i < outputSelect.options.length; i++){ - if (outputSelect.options[i].value==session.sink){ - outputSelect.options[i].selected=true; - sinkSet=true; + for (var i = 0; i < outputSelect.options.length; i++) { + if (outputSelect.options[i].value == session.sink) { + outputSelect.options[i].selected = true; + sinkSet = true; } } - if (sinkSet==false){ - if (outputSelect.options[0]){ - outputSelect.options[0].selected=true; + if (sinkSet == false) { + if (outputSelect.options[0]) { + outputSelect.options[0].selected = true; sinkSet = outputSelect.value; } } else { sinkSet = session.sink; } - if (sinkSet){ - session.videoElement.setSinkId(sinkSet).then(() => {}).catch(error => { errorlog(error);}); - for (UUID in session.rpcs){ + if (sinkSet) { + session.videoElement.setSinkId(sinkSet).then(() => {}).catch(error => { + errorlog(error); + }); + for (UUID in session.rpcs) { session.rpcs[UUID].videoElement.setSinkId(sinkSet).then(() => { - log("New Output Device for: "+UUID); + log("New Output Device for: " + UUID); }).catch(error => { errorlog(error); }); @@ -5615,325 +6538,498 @@ function resetupAudioOut(){ } } -function obfuscateURL(input){ - if (input.startsWith("https://obs.ninja/")){ +function obfuscateURL(input) { + if (input.startsWith("https://obs.ninja/")) { input = input.replace('https://obs.ninja/', ''); - } else if (input.startsWith("http://obs.ninja/")){ + } else if (input.startsWith("http://obs.ninja/")) { input = input.replace('http://obs.ninja/', ''); - } else if (input.startsWith("obs.ninja/")){ + } else if (input.startsWith("obs.ninja/")) { input = input.replace('obs.ninja/', ''); } - + input = input.replace('&view=', '&v='); input = input.replace('&view&', '&v&'); input = input.replace('?view&', '?v&'); input = input.replace('?view=', '?v='); - + input = input.replace('&videobitrate=', '&vb='); input = input.replace('?videobitrate=', '?vb='); input = input.replace('&bitrate=', '&vb='); input = input.replace('?bitrate=', '?vb='); - + input = input.replace('?audiodevice=', '?ad='); input = input.replace('&audiodevice=', '&ad='); - + input = input.replace('?label=', '?l='); input = input.replace('&label=', '&l='); - + input = input.replace('?stereo=', '?s='); input = input.replace('&stereo=', '&s='); input = input.replace('&stereo&', '&s&'); input = input.replace('?stereo&', '?s&'); - + input = input.replace('?webcam&', '?wc&'); input = input.replace('&webcam&', '&wc&'); - + input = input.replace('?remote=', '?rm='); input = input.replace('&remote=', '&rm='); - + input = input.replace('?password=', '?p='); input = input.replace('&password=', '&p='); - + input = input.replace('&maxvideobitrate=', '&mvb='); input = input.replace('?maxvideobitrate=', '?mvb='); - + input = input.replace('&maxbitrate=', '&mvb='); input = input.replace('?maxbitrate=', '?mvb='); - + input = input.replace('&height=', '&h='); input = input.replace('?height=', '?h='); - + input = input.replace('&width=', '&w='); input = input.replace('?width=', '?w='); - + input = input.replace('&quality=', '&q='); input = input.replace('?quality=', '?q='); - + input = input.replace('&cleanoutput=', '&clean='); input = input.replace('?cleanoutput=', '?clean='); - + input = input.replace('&maxviewers=', '&clean='); input = input.replace('?maxviewers=', '?clean='); - + input = input.replace('&framerate=', '&fr='); input = input.replace('?framerate=', '?fr='); - + input = input.replace('&fps=', '&fr='); input = input.replace('?fps=', '?fr='); - + input = input.replace('&permaid=', '&push='); input = input.replace('?permaid=', '?push='); - + input = input.replace('&roomid=', '&r='); input = input.replace('?roomid=', '?r='); - + input = input.replace('&room=', '&r='); input = input.replace('?room=', '?r='); - + log(input); - var key = "OBSNINJAFORLIFE"; + var key = "OBSNINJAFORLIFE"; var encrypted = CryptoJS.AES.encrypt(input, key); - var output = "https://invite.cam/"+ encrypted.toString(); + var output = "https://invite.cam/" + encrypted.toString(); return output; } document.addEventListener("visibilitychange", function() { log(document.hidden, document.visibilityState); - if ((iOS) || (iPad)){ // fixes a bug on iOS devices. Not need with other devices? + if ((iOS) || (iPad)) { // fixes a bug on iOS devices. Not need with other devices? if (document.visibilityState === 'visible') { - setTimeout(function(){resetupAudioOut();},500); + setTimeout(function() { + resetupAudioOut(); + }, 500); } } }); try { navigator.mediaDevices.ondevicechange = reconnectDevices; -} catch(e){errorlog(e);} +} catch (e) { + errorlog(e); +} function updateConnectionStatus() { - warnlog("Connection type changed from " + session.stats.network_type + " to " + Connection.effectiveType); - session.stats.network_type = Connection.effectiveType + " / " +Connection.type; + warnlog("Connection type changed from " + session.stats.network_type + " to " + Connection.effectiveType); + session.stats.network_type = Connection.effectiveType + " / " + Connection.type; } - -try{ - var Connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; - session.stats.network_type = Connection.effectiveType + " / " +Connection.type; - Connection.addEventListener('change', updateConnectionStatus); -} catch(e){} -var ScreeShareState=false; -async function toggleScreenShare(ele){ - if (ScreeShareState==false){ - await grabScreen(quality=0, audio=true, videoOnEnd=true).then(res=>{ - if (res!=false){ - ScreeShareState=true; +try { + var Connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + session.stats.network_type = Connection.effectiveType + " / " + Connection.type; + Connection.addEventListener('change', updateConnectionStatus); +} catch (e) {} + +var ScreenShareState = false; +var beforeScreenShare = null; // video +var screenShareAudioTrack = null; + +async function toggleScreenShare(reload = false) { //////////////////////////// + + if (reload) { + await grabScreen(quality = 0, audio = true, videoOnEnd = true).then(res => { + if (res != false) { + ScreenShareState = true; getById("screensharebutton").classList.add("float2"); getById("screensharebutton").classList.remove("float"); + enumerateDevices().then(gotDevices2).then(function() {}); } + }); - } else { - //activatedPreview = false; - toggleSettings(forceShow=true); - //grabVideo(eleName='videosource', selector="select#videoSource3"); - //getById("screensharebutton").classList.add("float"); - //getById("screensharebutton").classList.remove("float2"); - //ScreeShareState=false; + return; + } + + + if (ScreenShareState == false) { // adding a screen + + await grabScreen(quality = 0, audio = true, videoOnEnd = true).then(res => { + if (res != false) { + ScreenShareState = true; + getById("screensharebutton").classList.add("float2"); + getById("screensharebutton").classList.remove("float"); + enumerateDevices().then(gotDevices2).then(function() {}); + } + + }); + + } else { // removing a screen . ScreenShareState already true true ///////////////////////////////// + + + ScreenShareState = false; + pokeIframeAPI("screen-share-ended"); + + if (beforeScreenShare) { + + session.streamSrc.getAudioTracks().forEach(function(track) { // previous video track; saving it. Must remove the track at some point. + if (screenShareAudioTrack == track) { // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. + session.streamSrc.removeTrack(track); + //session.videoElement.srcObject = outboundAudioPipeline(session.streamSrc); + track.stop(); + } + }); + session.streamSrc.getVideoTracks().forEach(function(track) { + errorlog(track); + session.streamSrc.removeTrack(track); + track.stop(); + }); + + session.videoElement.srcObject.getVideoTracks().forEach(function(track) { + errorlog(track); + session.videoElement.srcObject.removeTrack(track); + track.stop(); + }); + + getById("screensharebutton").classList.add("float"); + getById("screensharebutton").classList.remove("float2"); + + session.streamSrc.addTrack(beforeScreenShare); // add back in the video track we had before we started screen sharing. It should be NULL if we changed the video track else where (such as via the settings). #TODO: + session.videoElement.srcObject.addTrack(beforeScreenShare); + + toggleVideoMute(true); + for (UUID in session.pcs) { + try { + if ((session.pcs[UUID].guest == true) && (session.roombitrate === 0)) { + log("room rate restriction detected. No videos will be published to other guests"); + } else if (session.pcs[UUID].allowVideo == true) { // allow + var senders = session.pcs[UUID].getSenders(); // for any connected peer, update the video they have if connected with a video already. + var added = false; + senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track) { + if (sender.track && sender.track.kind == "video") { + sender.replaceTrack(beforeScreenShare); // replace may not be supported by all browsers. eek. + sender.track.enabled = true; + added = true; + } + } + }); + if (added == false) { + session.pcs[UUID].addTrack(beforeScreenShare, stream); + } + } + } catch (e) { + errorlog(e); + } + } + beforeScreenShare = null; + } + toggleSettings(forceShow = true); + //enumerateDevices().then(gotDevices2).then(function(){ + //grabVideo(); + //grabAudio(); + // toggleSettings(forceShow=true); + //}); + + } } -async function grabScreen(quality=0, audio=true, videoOnEnd=false){ - if (!navigator.mediaDevices.getDisplayMedia){ - if (!(session.cleanOutput)){ - setTimeout(function(){alert("Sorry, your browser is not supported. Please use the desktop versions of Firefox or Chrome instead");},1); +async function grabScreen(quality = 0, audio = true, videoOnEnd = false) { + if (!navigator.mediaDevices.getDisplayMedia) { + if (!(session.cleanOutput)) { + setTimeout(function() { + alert("Sorry, your browser is not supported. Please use the desktop versions of Firefox or Chrome instead"); + }, 1); } return false; } - - - if (quality==0){ // I'm going to go with default quality in most cases, as I assume Dynamic screenshare is going to want low-fps / high def. - var width = {ideal: 1920}; - var height = {ideal: 1080}; - } else if (quality==1){ - var width = {ideal: 1280}; - var height = {ideal: 720}; - } else if (quality==2){ - var width = {ideal: 640}; - var height = {ideal: 360}; - } else if (quality>=3){ // lowest - var width = {ideal: 320}; - var height = {ideal: 180}; + + if (quality == 0) { // I'm going to go with default quality in most cases, as I assume Dynamic screenshare is going to want low-fps / high def. + var width = { + ideal: 1920 + }; + var height = { + ideal: 1080 + }; + } else if (quality == 1) { + var width = { + ideal: 1280 + }; + var height = { + ideal: 720 + }; + } else if (quality == 2) { + var width = { + ideal: 640 + }; + var height = { + ideal: 360 + }; + } else if (quality >= 3) { // lowest + var width = { + ideal: 320 + }; + var height = { + ideal: 180 + }; } - - if (session.width){ - width = {ideal: session.width}; + + if (session.width) { + width = { + ideal: session.width + }; } - if (session.height){ - height = {ideal: session.height}; + if (session.height) { + height = { + ideal: session.height + }; } var constraints = { // this part is a bit annoying. Do I use the same settings? I can add custom setting controls here later audio: { - echoCancellation: false, // For screen sharing, we want it off by default. - autoGainControl: false, - noiseSuppression: false - }, - video: {width: width, height: height, mediaSource: "screen"} + echoCancellation: false, // For screen sharing, we want it off by default. + autoGainControl: false + , noiseSuppression: false + } + , video: { + width: width + , height: height + , mediaSource: "screen" + } //,cursor: {exact: "none"} }; - if (session.echoCancellation===true){ - constraints.audio.echoCancellation=true; - } - if (session.autoGainControl===true){ - constraints.audio.autoGainControl=true; + if (session.echoCancellation === true) { + constraints.audio.echoCancellation = true; } - if (session.noiseSuppression===true){ - constraints.audio.noiseSuppression=true; + if (session.autoGainControl === true) { + constraints.audio.autoGainControl = true; } - if (audio==false){ - constraints.audio=false; + if (session.noiseSuppression === true) { + constraints.audio.noiseSuppression = true; } - - if (session.framerate){ + if (audio == false) { + constraints.audio = false; + } + + if (session.framerate) { constraints.video.frameRate = session.framerate; - } - - return navigator.mediaDevices.getDisplayMedia(constraints).then(function (stream) { + } + + return navigator.mediaDevices.getDisplayMedia(constraints).then(function(stream) { log("adding video tracks 2245"); - + var eleName = "videosource"; try { - if (session.streamSrc){ - session.streamSrc.getVideoTracks().forEach(function(track){ - track.stop(); + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(function(track) { + //track.stop(); + beforeScreenShare = track; session.streamSrc.removeTrack(track); log("stopping video track"); }); - session.videoElement.srcObject.getVideoTracks().forEach(function(track){ - track.stop(); + session.videoElement.srcObject.getVideoTracks().forEach(function(track) { + //track.stop(); session.videoElement.srcObject.removeTrack(track); - log("stopping video track"); + log("stopping video track 2"); }); } else { session.streamSrc = new MediaStream(); session.videoElement.srcObject = session.streamSrc; log("CREATE NEW STREAM"); } - } catch(e){ + } catch (e) { errorlog(e); } //session.videoElement.srcObject = session.streamSrc; - + // Let's not pass the AUDIO thru the webaudio filter. It's screen share after all. - + try { - stream.getVideoTracks()[0].onended = function () { // if screen share stops, - if (videoOnEnd==true){ + stream.getVideoTracks()[0].onended = function() { // if screen share stops, + + session.streamSrc.getVideoTracks().forEach(function(track) { + track.stop(); + session.streamSrc.removeTrack(track); + log("stopping video track 3"); + }); + session.videoElement.srcObject.getVideoTracks().forEach(function(track) { + track.stop(); + session.videoElement.srcObject.removeTrack(track); + log("stopping video track 4"); + }); + + ScreenShareState = false; + pokeIframeAPI("screen-share-ended"); + + getById("screensharebutton").classList.add("float"); + getById("screensharebutton").classList.remove("float2"); + + if (videoOnEnd == true) { //activatedPreview = false; - toggleSettings(forceShow=true); + + if (beforeScreenShare) { + session.streamSrc.addTrack(beforeScreenShare); + session.videoElement.srcObject.addTrack(beforeScreenShare); + if (beforeScreenShare.kind == "video") { + toggleVideoMute(true); + for (UUID in session.pcs) { + try { + if ((session.pcs[UUID].guest == true) && (session.roombitrate === 0)) { + log("room rate restriction detected. No videos will be published to other guests"); + } else if (session.pcs[UUID].allowVideo == true) { // allow + var senders = session.pcs[UUID].getSenders(); // for any connected peer, update the video they have if connected with a video already. + var added = false; + senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track) { + if (sender.track && sender.track.kind == "video") { + sender.replaceTrack(beforeScreenShare); // replace may not be supported by all browsers. eek. + sender.track.enabled = true; + added = true; + } + } + }); + if (added == false) { + session.pcs[UUID].addTrack(beforeScreenShare, stream); + } + } + } catch (e) { + errorlog(e); + } + } + } + beforeScreenShare = null; + } + + toggleSettings(forceShow = true); //grabVideo(eleName='videosource', selector="select#videoSource3"); - ScreeShareState=false; - getById("screensharebutton").classList.add("float"); - getById("screensharebutton").classList.remove("float2"); + + } else { grabScreen(); - } + } }; - } catch(e){log("No Video selected; screensharing?");} - - stream.getTracks().forEach(function(track){ + } catch (e) { + log("No Video selected; screensharing?"); + } + + stream.getTracks().forEach(function(track) { addScreenDevices(track); - + session.streamSrc.addTrack(track, stream); // Lets not add the audio to this preview; echo can be annoying - session.videoElement.srcObject.addTrack(track, stream); // - - if (track.kind == "video"){ + //session.videoElement.srcObject = outboundAudioPipeline(session.streamSrc); // TODO; this should probably be added. + session.videoElement.srcObject.addTrack(track, stream); // I should probably add the remote control to his ; #TODO: + + if (track.kind == "video") { toggleVideoMute(true); - for (UUID in session.pcs){ + for (UUID in session.pcs) { try { - if ((session.pcs[UUID].guest==true) && (session.roombitrate===0)) { + if ((session.pcs[UUID].guest == true) && (session.roombitrate === 0)) { log("room rate restriction detected. No videos will be published to other guests"); - } else if (session.pcs[UUID].allowVideo==true){ // allow + } else if (session.pcs[UUID].allowVideo == true) { // allow var senders = session.pcs[UUID].getSenders(); // for any connected peer, update the video they have if connected with a video already. - var added=false; + var added = false; senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track){ - if (sender.track && sender.track.kind == "video"){ - sender.replaceTrack(track); // replace may not be supported by all browsers. eek. + if (sender.track) { + if (sender.track && sender.track.kind == "video") { + sender.replaceTrack(track); // replace may not be supported by all browsers. eek. sender.track.enabled = true; - added=true; - } + added = true; + } } }); - if (added==false){ + if (added == false) { session.pcs[UUID].addTrack(track, stream); } } - } catch (e){ + } catch (e) { errorlog(e); } } } else { - toggleMute(true); // I might want to move this outside the loop, but whatever - for (UUID in session.pcs){ + toggleMute(true); // I might want to move this outside the loop, but whatever + for (UUID in session.pcs) { try { - if (session.pcs[UUID].allowAudio==true){ + if (session.pcs[UUID].allowAudio == true) { session.pcs[UUID].addTrack(track, stream); // If screen sharing, we will add audio; not replace. } - } catch (e){ + } catch (e) { errorlog(log); } } + screenShareAudioTrack = track; } }); - applyMirror(true,eleName); + applyMirror(true, eleName); return true; - }).catch(function(err){ - errorlog(err); /* handle the error */ - getById("screensharebutton").classList.add("float"); - getById("screensharebutton").classList.remove("float2"); - ScreeShareState=false; - if ((err.name == "NotAllowedError") || (err.name == "PermissionDeniedError")){ + }).catch(function(err) { + errorlog(err); + if ((err.name == "NotAllowedError") || (err.name == "PermissionDeniedError")) { // User Stopped it. } else { - if (audio==true){ - setTimeout(function(){grabScreen(quality, false);},1); + if (audio == true) { + setTimeout(function() { + grabScreen(quality, false); + }, 1); } - if (!(session.cleanOutput)){ - setTimeout(function(){alert(err);},1); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported + if (!(session.cleanOutput)) { + setTimeout(function() { + alert(err); + }, 1); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported } } return false; }); } -var getUserMediaRequestID=0; +var getUserMediaRequestID = 0; var grabVideoUserMediaTimeout = null; var grabVideoTimer = null; -async function grabVideo(quality=0, eleName='previewWebcam', selector="select#videoSourceSelect"){ - if( activatedPreview == true){log("activated preview return 2");return;} + +async function grabVideo(quality = 0, eleName = 'previewWebcam', selector = "select#videoSourceSelect") { + if (activatedPreview == true) { + log("activated preview return 2"); + return; + } activatedPreview = true; - log("Grabbing video: "+quality); - if (grabVideoTimer){ + log("Grabbing video: " + quality); + if (grabVideoTimer) { clearTimeout(grabVideoTimer); } - log("quality of grab:"+quality); - log("element:"+eleName); - + log("quality of grab:" + quality); + log("element:" + eleName); + try { - if (session.streamSrc){ - session.streamSrc.getVideoTracks().forEach(function(track){ + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(function(track) { session.streamSrc.removeTrack(track); track.stop(); log("track removed"); }); - if (session.videoElement.srcObject){ - session.videoElement.srcObject.getVideoTracks().forEach(function(track){ + if (session.videoElement.srcObject) { + session.videoElement.srcObject.getVideoTracks().forEach(function(track) { session.videoElement.srcObject.removeTrack(track); track.stop(); }); - + } } else { //log(session.videoElement.srcObject.getTracks()); @@ -5941,155 +7037,169 @@ async function grabVideo(quality=0, eleName='previewWebcam', selector="select#vi session.videoElement.srcObject = session.streamSrc; log("CREATE NEW STREAM"); } - } catch(e){ + } catch (e) { errorlog(e); } - - session.videoElement.controls=false; - + + session.videoElement.controls = false; + log("selector: " + selector); var videoSelect = document.querySelector(selector); log(videoSelect); - var mirror=false; - - if (videoSelect.value == "ZZZ"){ // if there is no video, or if manually set to audio ready, then do this step. + var mirror = false; + + if (videoSelect.value == "ZZZ") { // if there is no video, or if manually set to audio ready, then do this step. warnlog("ZZZ SET - so no VIDEO"); - if (eleName=="previewWebcam"){ - if (session.autostart){ - publishWebcam(); // no need to mirror as there is no video... + if (eleName == "previewWebcam") { + if (session.autostart) { + publishWebcam(); // no need to mirror as there is no video... return; } else { log("4462"); updateStats(); var gowebcam = getById("gowebcam"); - if (gowebcam){ + if (gowebcam) { gowebcam.disabled = false; gowebcam.dataset.ready = "true"; gowebcam.style.backgroundColor = "#3C3"; gowebcam.style.color = "black"; - gowebcam.style.fontWeight="bold"; + gowebcam.style.fontWeight = "bold"; gowebcam.innerHTML = "START"; - miniTranslate(gowebcam,"start"); + miniTranslate(gowebcam, "start"); } } - } else { // If they disabled the video but not in preview mode; but actualy live. We will want to remove the stream from the publishing - // we don't want to do this otherwise, as we are "replacing" the track in other cases. - // this does cause a problem, as previous bitrate settings & resolutions might not be applied if switched back.... must test - for (UUID in session.pcs){ + } else { // If they disabled the video but not in preview mode; but actualy live. We will want to remove the stream from the publishing + // we don't want to do this otherwise, as we are "replacing" the track in other cases. + // this does cause a problem, as previous bitrate settings & resolutions might not be applied if switched back.... must test + for (UUID in session.pcs) { var senders = session.pcs[UUID].getSenders(); // for any connected peer, update the video they have if connected with a video already. senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track && sender.track.kind == "video"){ + if (sender.track && sender.track.kind == "video") { sender.track.enabled = false; //session.pcs[UUID].removeTrack(sender); // replace may not be supported by all browsers. eek. errorlog("DELETED SENDER"); - } + } }); - + } } // end } else { - var sq=0; - if (session.quality===false){ + var sq = 0; + if (session.quality === false) { sq = session.quality_wb; - } else if (session.quality>2){ // 1080, 720, and 360p + } else if (session.quality > 2) { // 1080, 720, and 360p sq = 2; // hacking my own code. TODO: ugly, so I need to revisit this. } else { sq = session.quality; } - - if ((quality===false) || (quality -1){ - constraints.video.deviceId = {exact: videoSelect.value}; // Firefox is a dick. Needs it to be exact. - - } else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) { // NDI does not like "EXACT" + + if ((iOS) || (iPad)) { + constraints.video.deviceId = { + exact: videoSelect.value + }; // iPhone 6s compatible ? Needs to be exact for iPhone 6s + + } else if (sUsrAg.indexOf("Firefox") > -1) { + constraints.video.deviceId = { + exact: videoSelect.value + }; // Firefox is a dick. Needs it to be exact. + + } else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) { // NDI does not like "EXACT" constraints.video.deviceId = videoSelect.value; - + } else { - constraints.video.deviceId = {exact: videoSelect.value}; // Default. Should work for Logitech, etc. + constraints.video.deviceId = { + exact: videoSelect.value + }; // Default. Should work for Logitech, etc. } - - if (session.width){ - constraints.video.width = {exact: session.width}; // manually specified - so must be exact + + if (session.width) { + constraints.video.width = { + exact: session.width + }; // manually specified - so must be exact } - if (session.height){ - constraints.video.height = {exact: session.height}; + if (session.height) { + constraints.video.height = { + exact: session.height + }; } - if (session.framerate){ - constraints.video.frameRate = {exact: session.framerate}; - } else if (session.maxframerate){ - constraints.video.frameRate = {max: session.maxframerate}; + if (session.framerate) { + constraints.video.frameRate = { + exact: session.framerate + }; + } else if (session.maxframerate) { + constraints.video.frameRate = { + max: session.maxframerate + }; } - + var obscam = false; - log(videoSelect.options[videoSelect.selectedIndex].text); - if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS-Camera")){ // OBS Virtualcam - mirror=true; + log(videoSelect.options[videoSelect.selectedIndex].text); + if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) { // OBS Virtualcam + mirror = true; obscam = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")){ // OBS Virtualcam - mirror=true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) { // OBS Virtualcam + mirror = true; obscam = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" back")){ // Android - mirror=true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" rear")){ // Android - mirror=true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")){ // NDI Virtualcam - mirror=true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Back Camera")){ // iPhone and iOS - mirror=true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" back")) { // Android + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" rear")) { // Android + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) { // NDI Virtualcam + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Back Camera")) { // iPhone and iOS + mirror = true; } else { - mirror=false; + mirror = false; } session.mirrorExclude = mirror; - + log(constraints); clearTimeout(grabVideoUserMediaTimeout); - getUserMediaRequestID+=1; - grabVideoUserMediaTimeout = setTimeout(function(gumID){ - navigator.mediaDevices.getUserMedia(constraints).then(function(stream){ - - if (getUserMediaRequestID!==gumID){ + getUserMediaRequestID += 1; + grabVideoUserMediaTimeout = setTimeout(function(gumID) { + navigator.mediaDevices.getUserMedia(constraints).then(function(stream) { + + if (getUserMediaRequestID !== gumID) { errorlog("GET USER MEDIA CALL HAS EXPIRED"); return; } log("adding video tracks 2412"); - stream.getVideoTracks().forEach(function(track){ - if (!session.streamSrc){ + stream.getVideoTracks().forEach(function(track) { + if (!session.streamSrc) { session.streamSrc = new MediaStream(); } - if (!session.videoElement){ - if (document.getElementById("previewWebcam")){ + if (!session.videoElement) { + if (document.getElementById("previewWebcam")) { session.videoElement = document.getElementById("previewWebcam"); - } else if (document.getElementById("videosource")){ + } else if (document.getElementById("videosource")) { session.videoElement = document.getElementById("videosource"); } } - - if (session.effects){ + + if (session.effects) { applyEffects(eleName, track, stream); } else { log(session.videoElement); @@ -6097,148 +7207,148 @@ async function grabVideo(quality=0, eleName='previewWebcam', selector="select#vi session.videoElement.srcObject.addTrack(track, stream); // add video track to the preview video //session.videoElement.srcObject = outboundAudioPipeline(session.streamSrc); // WE DONT DO THIS UNLESS ADDING A NEW AUDIO TRACK ANDDDDD ARE PREPARED TO SETUP AUDIO RE_SENDERS } - + toggleVideoMute(true); - for (UUID in session.pcs){ + for (UUID in session.pcs) { try { - if (((iOS) || (iPad)) && (session.pcs[UUID].guest==true)){ + if (((iOS) || (iPad)) && (session.pcs[UUID].guest == true)) { warnlog("iOS and GUest detected"); - } else if ((session.pcs[UUID].guest==true) && (session.roombitrate===0)) { + } else if ((session.pcs[UUID].guest == true) && (session.roombitrate === 0)) { log("room rate restriction detected. No videos will be published to other guests"); - } else if (session.pcs[UUID].allowVideo==true){ // allow - + } else if (session.pcs[UUID].allowVideo == true) { // allow + // for any connected peer, update the video they have if connected with a video already. - var added=false; + var added = false; session.pcs[UUID].getSenders().forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - - if (sender.track && sender.track.kind == "video"){ - sender.replaceTrack(track); // replace may not be supported by all browsers. eek. + + if (sender.track && sender.track.kind == "video") { + sender.replaceTrack(track); // replace may not be supported by all browsers. eek. sender.track.enabled = true; - added=true; + added = true; } - + }); - if (added==false){ + if (added == false) { session.pcs[UUID].addTrack(track, stream); // can't replace, so adding } } - - } catch (e){ + + } catch (e) { errorlog(e); } } - + }); - - applyMirror(mirror,eleName); - - if (eleName=="previewWebcam"){ - if (session.autostart){ + + applyMirror(mirror, eleName); + + if (eleName == "previewWebcam") { + if (session.autostart) { publishWebcam(); } else { log("4620"); updateStats(obscam); var gowebcam = getById("gowebcam"); - if (gowebcam){ + if (gowebcam) { gowebcam.disabled = false; gowebcam.dataset.ready = "true"; gowebcam.style.backgroundColor = "#3C3"; gowebcam.style.color = "black"; - gowebcam.style.fontWeight="bold"; + gowebcam.style.fontWeight = "bold"; gowebcam.innerHTML = "START"; - miniTranslate(gowebcam,"start"); + miniTranslate(gowebcam, "start"); } } } - + // Once crbug.com/711524 is fixed, we won't need to wait anymore. This is // currently needed because capabilities can only be retrieved after the // device starts streaming. This happens after and asynchronously w.r.t. // getUserMedia() returns. - if (grabVideoTimer){ + if (grabVideoTimer) { clearTimeout(grabVideoTimer); - if (eleName=="previewWebcam"){ - session.videoElement.controls=true; + if (eleName == "previewWebcam") { + session.videoElement.controls = true; } } - if (getById("popupSelector_constraints_video")){ + if (getById("popupSelector_constraints_video")) { getById("popupSelector_constraints_video").innerHTML = ""; } - if (getById("popupSelector_constraints_audio")){ + if (getById("popupSelector_constraints_audio")) { getById("popupSelector_constraints_audio").innerHTML = ""; } - if (getById("popupSelector_constraints_loading")){ - getById("popupSelector_constraints_loading").style.display=""; + if (getById("popupSelector_constraints_loading")) { + getById("popupSelector_constraints_loading").style.display = ""; } - - grabVideoTimer = setTimeout(function(){ - if (getById("popupSelector_constraints_loading")){ - getById("popupSelector_constraints_loading").style.display="none"; + + grabVideoTimer = setTimeout(function() { + if (getById("popupSelector_constraints_loading")) { + getById("popupSelector_constraints_loading").style.display = "none"; } - if (eleName=="previewWebcam"){ - session.videoElement.controls=true; + if (eleName == "previewWebcam") { + session.videoElement.controls = true; } updateConstraintSliders(); - + dragElement(session.videoElement); - },1000); // focus - + }, 1000); // focus + log("DONE - found stream"); - }).catch(function(e){ + }).catch(function(e) { activatedPreview = false; errorlog(e); - if (e.name === "OverconstrainedError"){ + if (e.name === "OverconstrainedError") { errorlog(e.message); log("Resolution or framerate didn't work"); - } else if (e.name === "NotReadableError"){ - if (quality<=10){ - grabVideo(quality+1, eleName, selector); + } else if (e.name === "NotReadableError") { + if (quality <= 10) { + grabVideo(quality + 1, eleName, selector); } else { - if (!(session.cleanOutput)){ - if (iOS){ + if (!(session.cleanOutput)) { + if (iOS) { alert("An error occured. Closing existing tabs in Safari may solve this issue."); } else { alert("Error: Could not start video source.\n\nTypically this means the Camera is already be in use elsewhere. Most webcams can only be accessed by one program at a time.\n\nTry a different camera or perhaps try re-plugging in the device."); } } - activatedPreview=true; - if (getById('gowebcam')){ - getById('gowebcam').innerHTML="Problem with Camera"; + activatedPreview = true; + if (getById('gowebcam')) { + getById('gowebcam').innerHTML = "Problem with Camera"; } - + } return; - } else if (e.name === "NavigatorUserMediaError"){ - if (getById('gowebcam')){ - getById('gowebcam').innerHTML="Problem with Camera"; + } else if (e.name === "NavigatorUserMediaError") { + if (getById('gowebcam')) { + getById('gowebcam').innerHTML = "Problem with Camera"; } - if (!(session.cleanOutput)){ - alert("Unknown error: 'NavigatorUserMediaError'"); + if (!(session.cleanOutput)) { + alert("Unknown error: 'NavigatorUserMediaError'"); } return; - } else if (e.name === "timedOut"){ - activatedPreview=true; - if (getById('gowebcam')){ - getById('gowebcam').innerHTML="Problem with Camera"; + } else if (e.name === "timedOut") { + activatedPreview = true; + if (getById('gowebcam')) { + getById('gowebcam').innerHTML = "Problem with Camera"; } - if (!(session.cleanOutput)){ + if (!(session.cleanOutput)) { alert(e.message); } return; } else { errorlog("An unknown camera error occured"); } - - if (quality<=10){ - grabVideo(quality+1, eleName, selector); + + if (quality <= 10) { + grabVideo(quality + 1, eleName, selector); } else { errorlog("********Camera failed to work"); - activatedPreview=true; - if (getById('gowebcam')){ - getById('gowebcam').innerHTML="Problem with Camera"; + activatedPreview = true; + if (getById('gowebcam')) { + getById('gowebcam').innerHTML = "Problem with Camera"; } - if (!(session.cleanOutput)){ - if (session.width || session.height || session.framerate){ + if (!(session.cleanOutput)) { + if (session.width || session.height || session.framerate) { alert("Camera failed to load.\n\nPlease ensure your camera supports the resolution and framerate that has been manually specified. Perhaps use &quality=0 instead."); } else { alert("Camera failed to load.\n\nPlease make sure it is not already in use by another application.\n\nPlease make sure you have accepted the camera permissions."); @@ -6246,199 +7356,215 @@ async function grabVideo(quality=0, eleName='previewWebcam', selector="select#vi } } }); - },100, getUserMediaRequestID); + }, 100, getUserMediaRequestID); } } -async function grabAudio(eleName="previewWebcam", selector="#audioSource", trackid = null, override=false){ // trackid is the excluded track - if( activatedPreview == true){log("activated preview return 2");return;} +async function grabAudio(eleName = "previewWebcam", selector = "#audioSource", trackid = null, override = false) { // trackid is the excluded track + if (activatedPreview == true) { + log("activated preview return 2"); + return; + } activatedPreview = true; warnlog("GRABBING AUDIO"); - log("TRACK EXCLUDED:"+trackid); - - + log("TRACK EXCLUDED:" + trackid); + + try { - if (session.videoElement.srcObject){ - var audioSelect = document.querySelector(selector).querySelectorAll("input"); + if (session.videoElement.srcObject) { + var audioSelect = document.querySelector(selector).querySelectorAll("input"); var audioExcludeList = []; - for (var i=0; i { var good = false; - if (sender.track && sender.track.id && (sender.track.kind == "audio")){ - tracks.forEach(function(track){ - if (track.id == sender.track.id){ - good=true; + if (sender.track && sender.track.id && (sender.track.kind == "audio")) { + tracks.forEach(function(track) { + if (track.id == sender.track.id) { + good = true; } }); } else { // video or something else; ignore it. return; } - if (good){return;} + if (good) { + return; + } sender.track.enabled = false; //session.pcs[UUID].removeTrack(sender); // Apparently removeTrack causes renogiation; also kills send/recv. }); - - if (tracks.length){ - tracks.forEach(function(track){ + + if (tracks.length) { + tracks.forEach(function(track) { var matched = false; session.pcs[UUID].getSenders().forEach((sender) => { - if (sender.track && sender.track.id && (sender.track.kind !== "video")){ - warnlog(sender.track.id+" "+track.id); - if (sender.track.id == track.id){ + if (sender.track && sender.track.id && (sender.track.kind !== "video")) { + warnlog(sender.track.id + " " + track.id); + if (sender.track.id == track.id) { warnlog("MATCHED 1"); - matched=true; + matched = true; } } }); - if (matched){return;} + if (matched) { + return; + } var added = false; session.pcs[UUID].getSenders().forEach((sender) => { - if (added){return;} - if (sender.track && (sender.track.enabled==false)){ + if (added) { + return; + } + if (sender.track && (sender.track.enabled == false)) { sender.replaceTrack(track); sender.track.enabled = true; - added=true; + added = true; warnlog("ADDED 2"); } }); - if (added){return;} + if (added) { + return; + } var sender = session.pcs[UUID].addTrack(track, session.videoElement.srcObject); }); } else { session.pcs[UUID].getSenders().forEach((sender) => { - if (sender.track && sender.track.kind == "audio"){ + if (sender.track && sender.track.kind == "audio") { sender.track.enabled = false; // (trying this instead) //session.pcs[UUID].removeTrack(sender); // Apparently removeTrack causes renogiation; also kills send/recv. } }); } - + } } } - } catch (e){ + } catch (e) { errorlog(e); } var gowebcam = getById("gowebcam"); - if (gowebcam){ - gowebcam.disabled =false; + if (gowebcam) { + gowebcam.disabled = false; gowebcam.dataset.ready = "true"; gowebcam.style.backgroundColor = "#3C3"; gowebcam.style.color = "black"; - gowebcam.style.fontWeight="bold"; + gowebcam.style.fontWeight = "bold"; gowebcam.innerHTML = "START"; - miniTranslate(gowebcam,"start"); + miniTranslate(gowebcam, "start"); } } -function tryAgain(event){ // audio or video agnostic track reconnect ------------not actually in use,. maybe out of date +function tryAgain(event) { // audio or video agnostic track reconnect ------------not actually in use,. maybe out of date log("TRY AGAIN TRIGGERED"); - errorlog(event); -} - - -function enterPressedClick(event, ele){ - if (event.keyCode === 13){ - event.preventDefault(); - ele.click(); - } + warnlog(event); } -function enterPressed(event, callback){ - // Number 13 is the "Enter" key on the keyboard - if (event.keyCode === 13){ - event.preventDefault(); - callback(); - } + +function enterPressedClick(event, ele) { + if (event.keyCode === 13) { + event.preventDefault(); + ele.click(); + } +} + +function enterPressed(event, callback) { + // Number 13 is the "Enter" key on the keyboard + if (event.keyCode === 13) { + event.preventDefault(); + callback(); + } } function dragElement(elmnt) { - var millis = Date.now(); + var millis = Date.now(); try { var input = getById("zoomSlider"); var stream = elmnt.srcObject; try { var track0 = stream.getVideoTracks(); - } catch(e){return;} - - if (!(track0.length)){return;} - + } catch (e) { + return; + } + + if (!(track0.length)) { + return; + } + track0 = track0[0]; - if (track0.getCapabilities){ + if (track0.getCapabilities) { var capabilities = track0.getCapabilities(); var settings = track0.getSettings(); @@ -6454,44 +7580,48 @@ function dragElement(elmnt) { input.step = capabilities.zoom.step; input.value = settings.zoom; } - } catch(e){errorlog(e);return;} - + } catch (e) { + errorlog(e); + return; + } + log("drag on"); - elmnt.onmousedown = dragMouseDown; + elmnt.onmousedown = dragMouseDown; elmnt.onclick = onvideoclick; elmnt.ontouchstart = dragMouseDown; - + var pos0 = 1; - function onvideoclick(e){ + + function onvideoclick(e) { log(e); log("onvideoclick"); e = e || window.event; e.preventDefault(); return false; } - + function dragMouseDown(e) { log(e); log("dragMouseDown"); - + //closeDragElement(null); - + //elmnt.controls = false; e = e || window.event; e.preventDefault(); pos0 = input.value; - if(e.type == 'touchstart' || e.type == 'touchmove' || e.type == 'touchend' || e.type == 'touchcancel'){ - var touch = e.touches[0] ||e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; + if (e.type == 'touchstart' || e.type == 'touchmove' || e.type == 'touchend' || e.type == 'touchcancel') { + var touch = e.touches[0] || e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; pos3 = touch.clientX; pos4 = touch.clientY; - } else if (e.type == 'mousedown' || e.type == 'mouseup' || e.type == 'mousemove' || e.type == 'mouseover'|| e.type=='mouseout' || e.type=='mouseenter' || e.type=='mouseleave') { + } else if (e.type == 'mousedown' || e.type == 'mouseup' || e.type == 'mousemove' || e.type == 'mouseover' || e.type == 'mouseout' || e.type == 'mouseenter' || e.type == 'mouseleave') { pos3 = e.clientX; pos4 = e.clientY; } document.ontouchup = closeDragElement; document.onmouseup = closeDragElement; - + document.ontouchmove = elementDrag; document.onmousemove = elementDrag; } @@ -6500,28 +7630,35 @@ function dragElement(elmnt) { e = e || window.event; e.preventDefault(); // calculate the new cursor position: - - if ( Date.now() - millis < 50){ + + if (Date.now() - millis < 50) { return; } millis = Date.now(); - - if(e.type == 'touchstart' || e.type == 'touchmove' || e.type == 'touchend' || e.type == 'touchcancel'){ - var touch = e.touches[0] ||e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; - pos1 = touch.clientX; - pos2 = touch.clientY; - } else if (e.type == 'mousedown' || e.type == 'mouseup' || e.type == 'mousemove' || e.type == 'mouseover'|| e.type=='mouseout' || e.type=='mouseenter' || e.type=='mouseleave') { - pos1 = e.clientX; - pos2 = e.clientY; + + if (e.type == 'touchstart' || e.type == 'touchmove' || e.type == 'touchend' || e.type == 'touchcancel') { + var touch = e.touches[0] || e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; + pos1 = touch.clientX; + pos2 = touch.clientY; + } else if (e.type == 'mousedown' || e.type == 'mouseup' || e.type == 'mousemove' || e.type == 'mouseover' || e.type == 'mouseout' || e.type == 'mouseenter' || e.type == 'mouseleave') { + pos1 = e.clientX; + pos2 = e.clientY; } - - var zoom = parseFloat((pos4-pos2)*2/elmnt.offsetHeight); - - if (zoom>1){zoom =1.0;} - else if (zoom<-1){zoom=-1.0;} - input.value = zoom*(input.max - input.min) + input.min; - if (input.value != pos0){ - track0.applyConstraints({advanced: [ {zoom: input.value} ]}); + + var zoom = parseFloat((pos4 - pos2) * 2 / elmnt.offsetHeight); + + if (zoom > 1) { + zoom = 1.0; + } else if (zoom < -1) { + zoom = -1.0; + } + input.value = zoom * (input.max - input.min) + input.min; + if (input.value != pos0) { + track0.applyConstraints({ + advanced: [{ + zoom: input.value + }] + }); } } @@ -6539,42 +7676,42 @@ function dragElement(elmnt) { } } -function previewIframe(iframesrc){ // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: loadIframe();"> , but don't call it before the page loads. - +function previewIframe(iframesrc) { // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: loadIframe();"> , but don't call it before the page loads. + var iframe = document.createElement("iframe"); - iframe.allow="autoplay;camera;microphone"; - iframe.allowtransparency="true"; - iframe.allowfullscreen ="true"; - iframe.style.width="100%"; - iframe.style.height="100%"; + iframe.allow = "autoplay;camera;microphone"; + iframe.allowtransparency = "true"; + iframe.allowfullscreen = "true"; + iframe.style.width = "100%"; + iframe.style.height = "100%"; iframe.style.border = "10px dashed rgb(64 65 62)"; - - if (iframesrc==""){ - iframesrc="./"; + + if (iframesrc == "") { + iframesrc = "./"; } - + iframe.src = iframesrc; - getById("previewIframe").innerHTML =""; - getById("previewIframe").style.width="640px"; - getById("previewIframe").style.height="360px"; - getById("previewIframe").style.margin="auto"; + getById("previewIframe").innerHTML = ""; + getById("previewIframe").style.width = "640px"; + getById("previewIframe").style.height = "360px"; + getById("previewIframe").style.margin = "auto"; getById("previewIframe").appendChild(iframe); } -function loadIframe(iframesrc){ // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: loadIframe();"> , but don't call it before the page loads. - +function loadIframe(iframesrc) { // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: loadIframe();"> , but don't call it before the page loads. + var iframe = document.createElement("iframe"); - iframe.allow="autoplay;camera;microphone"; - iframe.allowtransparency="true"; - iframe.allowfullscreen ="true"; - iframe.style.width="100%"; - iframe.style.height="100%"; + iframe.allow = "autoplay;camera;microphone"; + iframe.allowtransparency = "true"; + iframe.allowfullscreen = "true"; + iframe.style.width = "100%"; + iframe.style.height = "100%"; iframe.style.border = "10px dashed rgb(64 65 62)"; - - if (iframesrc==""){ - iframesrc="./"; + + if (iframesrc == "") { + iframesrc = "./"; } - if (document.getElementById("mainmenu")){ + if (document.getElementById("mainmenu")) { var m = getById("mainmenu"); m.remove(); } @@ -6582,9 +7719,9 @@ function loadIframe(iframesrc){ // this is pretty important if you want to avoi return iframe } -function dropDownButtonAction(ele){ +function dropDownButtonAction(ele) { var ele = getById("dropButton"); - if (ele){ + if (ele) { ele.parentNode.removeChild(ele); getById('container-5').classList.remove('advanced'); getById('container-8').classList.remove('advanced'); @@ -6593,10 +7730,10 @@ function dropDownButtonAction(ele){ } } -function updateConstraintSliders(){ +function updateConstraintSliders() { log("updateConstraintSliders"); - if (session.roomid!==false && session.roomid!=="" && session.director!==true && session.forceMediaSettings==false){ - if (session.controlRoomBitrate===true){ + if (session.roomid !== false && session.roomid !== "" && session.director !== true && session.forceMediaSettings == false) { + if (session.controlRoomBitrate === true) { listCameraSettings(); } } else { @@ -6606,91 +7743,99 @@ function updateConstraintSliders(){ //checkIfPIP(); // this doesn't actually work on iOS still, so whatever. } -function checkIfPIP(){ - try{ +function checkIfPIP() { + try { if (session.videoElement && ((session.videoElement.webkitSupportsPresentationMode && typeof session.videoElement.webkitSetPresentationMode === "function") || (document.pictureInPictureEnabled || !videoElement.disablePictureInPicture))) { // Toggle PiP when the user clicks the button. - + getById("pIpStartButton").addEventListener("click", function(event) { - // if ( (document.pictureInPictureEnabled || !videoElement.disablePictureInPicture)){ - //session.videoElement.requestPictureInPicture(); - // } else { - session.videoElement.webkitSetPresentationMode(session.videoElement.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture"); - // } + // if ( (document.pictureInPictureEnabled || !videoElement.disablePictureInPicture)){ + //session.videoElement.requestPictureInPicture(); + // } else { + session.videoElement.webkitSetPresentationMode(session.videoElement.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture"); + // } }); - getById("pIpStartButton").style.display="inline-block"; - } - } catch(e){ + getById("pIpStartButton").style.display = "inline-block"; + } + } catch (e) { errorlog(e); } } -function listAudioSettingsPrep(){ +function listAudioSettingsPrep() { try { var tracks = session.streamSrc.getAudioTracks(); - if (!tracks.length){ + if (!tracks.length) { warnlog("session.streamSrc contains no audio tracks"); return; } - } catch(e){ + } catch (e) { warnlog(e); return; } - + var data = []; - - for (var i =0;i0){ - log("FINAL:"+ Final_transcript); + + if (Final_transcript.length > 0) { + log("FINAL:" + Final_transcript); try { var data = {}; data.isFinal = true; data.transcript = Final_transcript; data.counter = TranscriptionCounter; session.sendMessage(data); - TranscriptionCounter+=1; - Final_transcript=""; - Interim_transcript=""; - } catch(e){errorlog(e);} - + TranscriptionCounter += 1; + Final_transcript = ""; + Interim_transcript = ""; + } catch (e) { + errorlog(e); + } + } else { try { var data = {}; @@ -6774,21 +7926,21 @@ function setupClosedCaptions(){ data.transcript = Interim_transcript; data.counter = TranscriptionCounter; session.sendMessage(data); - } catch(e){ + } catch (e) { errorlog(e); - Interim_transcript=""; + Interim_transcript = ""; } } }; - + Recognition.start(); } } -function requestVideoRecord(ele){ +function requestVideoRecord(ele) { var UUID = ele.dataset.UUID - if (ele.classList.contains("pressed")){ + if (ele.classList.contains("pressed")) { var msg = {}; msg.requestVideoRecord = false; msg.UUID = UUID; @@ -6798,21 +7950,23 @@ function requestVideoRecord(ele){ var msg = {}; msg.requestVideoRecord = true; msg.UUID = UUID; - var bitrate = prompt("What bitrate would you like to record at? (kbps)",6000); - if (bitrate){ + var bitrate = prompt("What bitrate would you like to record at? (kbps)", 6000); + if (bitrate) { msg.value = bitrate; session.sendRequest(msg, msg.UUID); ele.classList.add("pressed"); } } } -function changeOrder(value, UUID){ + +function changeOrder(value, UUID) { var msg = {}; msg.changeOrder = value; msg.UUID = UUID; session.sendRequest(msg, msg.UUID); } -function requestVideoHack(keyname,value, UUID){ + +function requestVideoHack(keyname, value, UUID) { var msg = {}; msg.requestVideoHack = true; msg.keyname = keyname; @@ -6820,7 +7974,8 @@ function requestVideoHack(keyname,value, UUID){ msg.UUID = UUID; session.sendRequest(msg, msg.UUID); } -function requestAudioHack(keyname,value, UUID, track=0){ // updateCameraConstraints + +function requestAudioHack(keyname, value, UUID, track = 0) { // updateCameraConstraints var msg = {}; msg.requestAudioHack = true; msg.keyname = keyname; @@ -6829,7 +7984,8 @@ function requestAudioHack(keyname,value, UUID, track=0){ // updateCameraConstra msg.track = track; session.sendRequest(msg, msg.UUID); } -function requestChangeEQ(keyname,value, UUID, track=0){ // updateCameraConstraints + +function requestChangeEQ(keyname, value, UUID, track = 0) { // updateCameraConstraints var msg = {}; msg.requestChangeEQ = true; msg.keyname = keyname; @@ -6839,196 +7995,251 @@ function requestChangeEQ(keyname,value, UUID, track=0){ // updateCameraConstrai session.sendRequest(msg, msg.UUID); } -function updateDirectorsAudio(dataN, UUID){ +function requestChangeLowcut(value, UUID, track = 0) { // updateCameraConstraints + var msg = {}; + msg.requestChangeLowcut = true; + msg.value = value; + msg.UUID = UUID; + msg.track = track; + session.sendRequest(msg, msg.UUID); +} + +function updateDirectorsAudio(dataN, UUID) { var audioEle = document.createElement("div"); - getById("advanced_audio_director_"+UUID).innerHTML = ""; - getById("advanced_audio_director_"+UUID).className = ""; - + getById("advanced_audio_director_" + UUID).innerHTML = ""; + getById("advanced_audio_director_" + UUID).className = ""; + //log(dataN); - if (!dataN.length){ + if (!dataN.length) { return; } - - for (var n=0;n1){ + + if (data.audioConstraints[i].length > 1) { for (var opts in data.audioConstraints[i]) { - log(opts); - var opt = new Option(data.audioConstraints[i][opts], data.audioConstraints[i][opts]); - input.options.add(opt); - if (i in data.currentAudioConstraints){ - if (data.audioConstraints[i][opts] == data.currentAudioConstraints[i]){ - opt.selected=true; + log(opts); + var opt = new Option(data.audioConstraints[i][opts], data.audioConstraints[i][opts]); + input.options.add(opt); + if (i in data.currentAudioConstraints) { + if (data.audioConstraints[i][opts] == data.currentAudioConstraints[i]) { + opt.selected = true; + } } - } } - } else if (i.toLowerCase == "torch"){ + } else if (i.toLowerCase == "torch") { var opt = new Option("Off", false); input.options.add(opt); opt = new Option("On", true); @@ -7036,15 +8247,15 @@ function updateDirectorsAudio(dataN, UUID){ } else { continue; } - - input.id = "constraints_"+i; - input.className="constraintCameraInput"; - input.name = "constraints_"+i; + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; input.style = "display:inline; padding:2px; margin:0 10px;"; input.dataset.keyname = i; input.dataset.track = n; input.dataset.UUID = UUID; - input.onchange = function(e){ + input.onchange = function(e) { //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; //updateAudioConstraints(e.target.dataset.keyname, e.target.value); requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.track); @@ -7053,31 +8264,31 @@ function updateDirectorsAudio(dataN, UUID){ audioEle.appendChild(div); div.appendChild(label); div.appendChild(input); - } else if (typeof data.audioConstraints[i] === 'boolean'){ - + } else if (typeof data.audioConstraints[i] === 'boolean') { + var div = document.createElement("div"); var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerText = i+":"; - label.style="display:inline-block; padding:0;margin: 5px 0px 9px;"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerText = i + ":"; + label.style = "display:inline-block; padding:0;margin: 5px 0px 9px;"; label.dataset.keyname = i; var input = document.createElement("select"); var c = document.createElement("option"); - + var opt = new Option("Off", false); input.options.add(opt); opt = new Option("On", true); input.options.add(opt); - - input.id = "constraints_"+i; - input.className="constraintCameraInput"; - input.name = "constraints_"+i; + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; input.style = "display:inline; padding:2px; margin:0 10px;"; input.dataset.keyname = i; input.dataset.track = n; input.dataset.UUID = UUID; - input.onchange = function(e){ + input.onchange = function(e) { //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; //updateAudioConstraints(e.target.dataset.keyname, e.target.value); requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.track); @@ -7087,96 +8298,112 @@ function updateDirectorsAudio(dataN, UUID){ div.appendChild(label); div.appendChild(input); } - } catch(e){errorlog(e);} + } catch (e) { + errorlog(e); + } } - getById("advanced_audio_director_"+UUID).appendChild(audioEle); + getById("advanced_audio_director_" + UUID).appendChild(audioEle); } } -function updateDirectorsVideo(data, UUID){ +function updateDirectorsVideo(data, UUID) { var videoEle = document.createElement("div"); - if (data.trackLabel){ + if (data.trackLabel) { var label = document.createElement("span"); label.innerText = data.trackLabel; label.style.marginBottom = "10px"; label.style.display = "block"; videoEle.appendChild(label); } - for (var i in data.cameraConstraints){ + for (var i in data.cameraConstraints) { try { log(i); log(data.cameraConstraints[i]); - if ((typeof data.cameraConstraints[i] ==='object') && (data.cameraConstraints[i] !== null) && ("max" in data.cameraConstraints[i]) && ("min" in data.cameraConstraints[i])){ - if (i==="aspectRatio"){continue;} - else if (i==="width"){continue;} - else if (i==="height"){continue;} - else if (i==="frameRate"){continue;} - else if (i==="latency"){continue;} - else if (i==="sampleRate"){continue;} - + if ((typeof data.cameraConstraints[i] === 'object') && (data.cameraConstraints[i] !== null) && ("max" in data.cameraConstraints[i]) && ("min" in data.cameraConstraints[i])) { + if (i === "aspectRatio") { + continue; + } else if (i === "width") { + continue; + } else if (i === "height") { + continue; + } else if (i === "frameRate") { + continue; + } else if (i === "latency") { + continue; + } else if (i === "sampleRate") { + continue; + } else if (i === "channelCount") { + continue; + } + var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerText = i+":"; - + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerText = i + ":"; + var input = document.createElement("input"); input.min = data.cameraConstraints[i].min; input.max = data.cameraConstraints[i].max; - - if (parseFloat(input.min) == parseFloat(input.max)){continue;} - - - - if (i in data.currentCameraConstraints){ + + if (parseFloat(input.min) == parseFloat(input.max)) { + continue; + } + + + if (i in data.currentCameraConstraints) { input.value = data.currentCameraConstraints[i]; - label.innerText = i+": "+data.currentCameraConstraints[i]; + label.innerText = i + ": " + data.currentCameraConstraints[i]; + label.title = "Previously was: " + data.currentCameraConstraints[i]; + input.title = "Previously was: " + data.currentCameraConstraints[i]; } else { label.innerText = i; } - if ("step" in data.cameraConstraints[i]){ + if ("step" in data.cameraConstraints[i]) { input.step = data.cameraConstraints[i].step; } input.type = "range"; input.dataset.keyname = i; - input.id = "constraints_"+i; - input.style="display:block; width:100%;margin: 8px 0;"; - input.name = "constraints_"+i; - - - input.onchange = function(e){ - getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;margin: 8px 0;"; + input.name = "constraints_" + i; + + + input.onchange = function(e) { + getById("label_" + e.target.dataset.keyname).innerText = e.target.dataset.keyname + ": " + e.target.value; //updateVideoConstraints(e.target.dataset.keyname, e.target.value); requestVideoHack(e.target.dataset.keyname, e.target.value, UUID); }; - - + + videoEle.appendChild(label); videoEle.appendChild(input); - } else if ((typeof data.cameraConstraints[i] ==='object') && (data.cameraConstraints[i] !== null)){ - if (i == "resizeMode"){continue;} - + } else if ((typeof data.cameraConstraints[i] === 'object') && (data.cameraConstraints[i] !== null)) { + if (i == "resizeMode") { + continue; + } + var div = document.createElement("div"); var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerText = i+":"; - label.style="display:inline-block; padding:0;margin: 5px 0px 9px;"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerText = i + ":"; + label.style = "display:inline-block; padding:0;margin: 5px 0px 9px;"; label.dataset.keyname = i; var input = document.createElement("select"); var c = document.createElement("option"); - - if (data.cameraConstraints[i].length>1){ + + if (data.cameraConstraints[i].length > 1) { for (var opts in data.cameraConstraints[i]) { - log(opts); - var opt = new Option(data.cameraConstraints[i][opts], data.cameraConstraints[i][opts]); - input.options.add(opt); - if (i in data.currentCameraConstraints){ - if (data.cameraConstraints[i][opts] == data.currentCameraConstraints[i]){ - opt.selected=true; + log(opts); + var opt = new Option(data.cameraConstraints[i][opts], data.cameraConstraints[i][opts]); + input.options.add(opt); + if (i in data.currentCameraConstraints) { + if (data.cameraConstraints[i][opts] == data.currentCameraConstraints[i]) { + opt.selected = true; + } } - } } - } else if (i.toLowerCase == "torch"){ + } else if (i.toLowerCase == "torch") { var opt = new Option("Off", false); input.options.add(opt); opt = new Option("On", true); @@ -7184,13 +8411,13 @@ function updateDirectorsVideo(data, UUID){ } else { continue; } - - input.id = "constraints_"+i; - input.className="constraintCameraInput"; - input.name = "constraints_"+i; + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; input.style = "display:inline; padding:2px; margin:0 10px;"; input.dataset.keyname = i; - input.onchange = function(e){ + input.onchange = function(e) { //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; //updateVideoConstraints(e.target.dataset.keyname, e.target.value); requestVideoHack(e.target.dataset.keyname, e.target.value, UUID); @@ -7199,29 +8426,29 @@ function updateDirectorsVideo(data, UUID){ videoEle.appendChild(div); div.appendChild(label); div.appendChild(input); - } else if (typeof data.cameraConstraints[i] === 'boolean'){ - + } else if (typeof data.cameraConstraints[i] === 'boolean') { + var div = document.createElement("div"); var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerText = i+":"; - label.style="display:inline-block; padding:0;margin: 5px 0px 9px;"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerText = i + ":"; + label.style = "display:inline-block; padding:0;margin: 5px 0px 9px;"; label.dataset.keyname = i; var input = document.createElement("select"); var c = document.createElement("option"); - + var opt = new Option("Off", false); input.options.add(opt); opt = new Option("On", true); input.options.add(opt); - - input.id = "constraints_"+i; - input.className="constraintCameraInput"; - input.name = "constraints_"+i; + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; input.style = "display:inline; padding:2px; margin:0 10px;"; input.dataset.keyname = i; - input.onchange = function(e){ + input.onchange = function(e) { //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; //updateVideoConstraints(e.target.dataset.keyname, e.target.value); requestVideoHack(e.target.dataset.keyname, e.target.value, UUID); @@ -7231,23 +8458,25 @@ function updateDirectorsVideo(data, UUID){ div.appendChild(label); div.appendChild(input); } - } catch(e){errorlog(e);} + } catch (e) { + errorlog(e); + } } - - getById("advanced_video_director_"+UUID).innerHTML = ""; - getById("advanced_video_director_"+UUID).appendChild(videoEle); - getById("advanced_video_director_"+UUID).className = ""; + + getById("advanced_video_director_" + UUID).innerHTML = ""; + getById("advanced_video_director_" + UUID).appendChild(videoEle); + getById("advanced_video_director_" + UUID).className = ""; } /////// -function listAudioSettings(){ +function listAudioSettings() { getById("popupSelector_constraints_audio").innerHTML = ""; try { var track0 = session.streamSrc.getAudioTracks(); - if (track0.length){ + if (track0.length) { track0 = track0[0]; - if (track0.getCapabilities){ + if (track0.getCapabilities) { session.audioConstraints = track0.getCapabilities(); } log(session.audioConstraints); @@ -7255,215 +8484,268 @@ function listAudioSettings(){ warnlog("session.streamSrc contains no audio tracks"); return; } - } catch(e){ + } catch (e) { warnlog("session.streamSrc contains no audio tracks"); errorlog(e); return; } try { - if (track0.getSettings){ + if (track0.getSettings) { session.currentAudioConstraints = track0.getSettings(); } - } catch(e){ + } catch (e) { errorlog(e); } ////// - if (session.equalizer){ - if (getById("popupSelector_constraints_audio").style.display == "none"){ - getById("advancedOptionsAudio").style.display="inline-block"; + + if (session.lowcut) { + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-block"; } - + + var label = document.createElement("label"); + var i = "Low_Cut"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerText = "Low Cut:"; + + var input = document.createElement("input"); + input.min = 50; + input.max = 400; + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].lowcut1.frequency) { + input.value = session.webAudios[webAudio].lowcut1.frequency.value; + label.innerHTML += " " + session.webAudios[webAudio].lowcut1.frequency.value; + } + } + + input.onchange = function(e) { + getById("label_" + e.target.dataset.keyname).innerHTML = e.target.dataset.labelname + " " + e.target.value; + changeLowCut(e.target.value, 0); + }; + + getById("popupSelector_constraints_audio").appendChild(label); + getById("popupSelector_constraints_audio").appendChild(input); + } + + if (session.equalizer) { + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-block"; + } + var label = document.createElement("label"); var i = "Low_EQ"; - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; label.innerHTML = "Low EQ:"; - + var input = document.createElement("input"); input.min = -50; input.max = 50; - + input.type = "range"; input.dataset.keyname = i; input.dataset.labelname = label.innerHTML; - input.id = "constraints_"+i; - input.style="display:block; width:100%;"; - input.name = "constraints_"+i; - - for (var webAudio in session.webAudios){ - if (session.webAudios[webAudio].lowEQ.gain){ + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].lowEQ.gain) { input.value = session.webAudios[webAudio].lowEQ.gain.value; - label.innerHTML +=" "+session.webAudios[webAudio].lowEQ.gain.value; + label.innerHTML += " " + session.webAudios[webAudio].lowEQ.gain.value; } } - input.onchange = function(e){ - getById("label_"+e.target.dataset.keyname).innerHTML = e.target.dataset.labelname+" "+e.target.value; - changeLowEQ( e.target.value, 0); + input.onchange = function(e) { + getById("label_" + e.target.dataset.keyname).innerHTML = e.target.dataset.labelname + " " + e.target.value; + changeLowEQ(e.target.value, 0); }; - + getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(input); + getById("popupSelector_constraints_audio").appendChild(input); // - if (getById("popupSelector_constraints_audio").style.display == "none"){ - getById("advancedOptionsAudio").style.display="inline-block"; + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-block"; } - + var label = document.createElement("label"); var i = "Mid_EQ"; - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; label.innerHTML = "Mid EQ:"; - + var input = document.createElement("input"); input.min = -50; input.max = 50; - + input.type = "range"; input.dataset.keyname = i; input.dataset.labelname = label.innerHTML; - input.id = "constraints_"+i; - input.style="display:block; width:100%;"; - input.name = "constraints_"+i; - - - for (var webAudio in session.webAudios){ - if (session.webAudios[webAudio].midEQ.gain){ + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].midEQ.gain) { input.value = session.webAudios[webAudio].midEQ.gain.value; - label.innerHTML +=" "+session.webAudios[webAudio].midEQ.gain.value; + label.innerHTML += " " + session.webAudios[webAudio].midEQ.gain.value; } } - - - input.onchange = function(e){ - getById("label_"+e.target.dataset.keyname).innerHTML = e.target.dataset.labelname+" "+e.target.value; - changeMidEQ( e.target.value, 0); + + input.onchange = function(e) { + getById("label_" + e.target.dataset.keyname).innerHTML = e.target.dataset.labelname + " " + e.target.value; + changeMidEQ(e.target.value, 0); }; - + getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(input); + getById("popupSelector_constraints_audio").appendChild(input); // - if (getById("popupSelector_constraints_audio").style.display == "none"){ - getById("advancedOptionsAudio").style.display="inline-block"; + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-block"; } - + var label = document.createElement("label"); var i = "High_EQ"; - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; label.innerHTML = "High EQ:"; - + var input = document.createElement("input"); input.min = -50; input.max = 50; - - + + input.type = "range"; input.dataset.keyname = i; input.dataset.labelname = label.innerHTML; - input.id = "constraints_"+i; - input.style="display:block; width:100%;"; - input.name = "constraints_"+i; - - for (var webAudio in session.webAudios){ - if (session.webAudios[webAudio].highEQ.gain){ + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].highEQ.gain) { input.value = session.webAudios[webAudio].highEQ.gain.value; - label.innerHTML +=" "+session.webAudios[webAudio].highEQ.gain.value; + label.innerHTML += " " + session.webAudios[webAudio].highEQ.gain.value; } } - - - input.onchange = function(e){ - getById("label_"+e.target.dataset.keyname).innerHTML = e.target.dataset.labelname+" "+e.target.value; - changeHighEQ( e.target.value, 0); + + input.onchange = function(e) { + getById("label_" + e.target.dataset.keyname).innerHTML = e.target.dataset.labelname + " " + e.target.value; + changeHighEQ(e.target.value, 0); }; - + getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(input); + getById("popupSelector_constraints_audio").appendChild(input); } //////// - for (var i in session.audioConstraints){ + for (var i in session.audioConstraints) { try { log(i); log(session.audioConstraints[i]); - if ((typeof session.audioConstraints[i] ==='object') && (session.audioConstraints[i] !== null) && ("max" in session.audioConstraints[i]) && ("min" in session.audioConstraints[i])){ - if (i==="aspectRatio"){continue;} - else if (i==="width"){continue;} - else if (i==="height"){continue;} - else if (i==="frameRate"){continue;} - else if (i==="latency"){continue;} - else if (i==="sampleRate"){continue;} - + if ((typeof session.audioConstraints[i] === 'object') && (session.audioConstraints[i] !== null) && ("max" in session.audioConstraints[i]) && ("min" in session.audioConstraints[i])) { + if (i === "aspectRatio") { + continue; + } else if (i === "width") { + continue; + } else if (i === "height") { + continue; + } else if (i === "frameRate") { + continue; + } else if (i === "latency") { + continue; + } else if (i === "sampleRate") { + continue; + } else if (i === "channelCount") { + continue; + } + var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerHTML = i+":"; - + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = i + ":"; + + var input = document.createElement("input"); input.min = session.audioConstraints[i].min; input.max = session.audioConstraints[i].max; - - if (parseFloat(input.min) == parseFloat(input.max)){continue;} - - if (getById("popupSelector_constraints_audio").style.display == "none"){ - getById("advancedOptionsAudio").style.display="inline-block"; + + if (parseFloat(input.min) == parseFloat(input.max)) { + continue; } - - - if (i in session.currentAudioConstraints){ + + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-block"; + } + + + if (i in session.currentAudioConstraints) { input.value = session.currentAudioConstraints[i]; - label.innerHTML = i+": "+session.currentAudioConstraints[i]; + label.innerHTML = i + ": " + session.currentAudioConstraints[i]; + label.title = "Previously was: " + session.currentAudioConstraints[i]; + input.title = "Previously was: " + session.currentAudioConstraints[i]; } else { label.innerHTML = i; } - if ("step" in session.audioConstraints[i]){ + if ("step" in session.audioConstraints[i]) { input.step = session.audioConstraints[i].step; } input.type = "range"; input.dataset.keyname = i; - input.id = "constraints_"+i; - input.style="display:block; width:100%;"; - input.name = "constraints_"+i; - - - input.onchange = function(e){ - getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + + input.onchange = function(e) { + getById("label_" + e.target.dataset.keyname).innerHTML = e.target.dataset.keyname + ": " + e.target.value; //updateAudioConstraints(e.target.dataset.keyname, e.target.value); applyAudioHack(track0, e.target.dataset.keyname, e.target.value); }; - - + + getById("popupSelector_constraints_audio").appendChild(label); getById("popupSelector_constraints_audio").appendChild(input); - } else if ((typeof session.audioConstraints[i] ==='object') && (session.audioConstraints[i] !== null)){ - if (i == "resizeMode"){continue;} - + } else if ((typeof session.audioConstraints[i] === 'object') && (session.audioConstraints[i] !== null)) { + if (i == "resizeMode") { + continue; + } + var div = document.createElement("div"); var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerHTML = i+":"; - label.style="display:inline-block; padding:0;margin: 15px 0px 29px;"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = i + ":"; + label.style = "display:inline-block; padding:0;margin: 15px 0px 29px;"; label.dataset.keyname = i; var input = document.createElement("select"); var c = document.createElement("option"); - - if (session.audioConstraints[i].length>1){ + + if (session.audioConstraints[i].length > 1) { for (var opts in session.audioConstraints[i]) { - log(opts); - var opt = new Option(session.audioConstraints[i][opts], session.audioConstraints[i][opts]); - input.options.add(opt); - - if (i in session.currentAudioConstraints){ - if (session.audioConstraints[i][opts] == session.currentAudioConstraints[i]){ - opt.selected=true; + log(opts); + var opt = new Option(session.audioConstraints[i][opts], session.audioConstraints[i][opts]); + input.options.add(opt); + + if (i in session.currentAudioConstraints) { + if (session.audioConstraints[i][opts] == session.currentAudioConstraints[i]) { + opt.selected = true; + } } - } - + } - } else if (i.toLowerCase == "torch"){ + } else if (i.toLowerCase == "torch") { var opt = new Option("Off", false); input.options.add(opt); opt = new Option("On", true); @@ -7471,246 +8753,278 @@ function listAudioSettings(){ } else { continue; } - - if (getById("popupSelector_constraints_audio").style.display == "none"){ - getById("advancedOptionsAudio").style.display="inline-block"; + + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-block"; } - - input.id = "constraints_"+i; - input.className="constraintCameraInput"; - input.name = "constraints_"+i; + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; input.style = "display:inline; padding:2px; margin:0 10px;"; input.dataset.keyname = i; - input.onchange = function(e){ + input.onchange = function(e) { //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; //updateAudioConstraints(e.target.dataset.keyname, e.target.value); - applyAudioHack(track0,e.target.dataset.keyname, e.target.value); + applyAudioHack(track0, e.target.dataset.keyname, e.target.value); log(e.target.dataset.keyname, e.target.value); }; getById("popupSelector_constraints_audio").appendChild(div); div.appendChild(label); div.appendChild(input); - } else if (typeof session.audioConstraints[i] === 'boolean'){ - + } else if (typeof session.audioConstraints[i] === 'boolean') { + var div = document.createElement("div"); var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerHTML = i+":"; - label.style="display:inline-block; padding:0;margin: 15px 0px 29px;"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = i + ":"; + label.style = "display:inline-block; padding:0;margin: 15px 0px 29px;"; label.dataset.keyname = i; var input = document.createElement("select"); var c = document.createElement("option"); - + var opt = new Option("Off", false); input.options.add(opt); opt = new Option("On", true); input.options.add(opt); - - if (getById("popupSelector_constraints_audio").style.display == "none"){ - getById("advancedOptionsAudio").style.display="inline-block"; + + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-block"; } - - input.id = "constraints_"+i; - input.className="constraintCameraInput"; - input.name = "constraints_"+i; + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; input.style = "display:inline; padding:2px; margin:0 10px;"; input.dataset.keyname = i; - input.onchange = function(e){ + input.onchange = function(e) { //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; //updateAudioConstraints(e.target.dataset.keyname, e.target.value); - applyAudioHack(track0,e.target.dataset.keyname, e.target.value); + applyAudioHack(track0, e.target.dataset.keyname, e.target.value); log(e.target.dataset.keyname, e.target.value); }; getById("popupSelector_constraints_audio").appendChild(div); div.appendChild(label); div.appendChild(input); } - } catch(e){errorlog(e);} - + } catch (e) { + errorlog(e); + } + } } -function applyAudioHack(track, constraint, value=null){ - if (value == parseFloat(value)){ +function applyAudioHack(track, constraint, value = null) { + if (value == parseFloat(value)) { value = parseFloat(value); - value = {exact: value}; - } else if (value == "true"){ + value = { + exact: value + }; + } else if (value == "true") { value = true; - } else if (value == "false"){ + } else if (value == "false") { value = false; } log(constraint); - var new_constraints = Object.assign(track.getSettings(), {[constraint]:value}, ); - new_constraints = {audio: new_constraints, video:false}; + var new_constraints = Object.assign(track.getSettings(), { + [constraint]: value + }, ); + new_constraints = { + audio: new_constraints + , video: false + }; log(new_constraints); - activatedPreview=false; - enumerateDevices().then(gotDevices2).then(function(){grabAudio("videosource","#audioSource3", null, new_constraints);}); - + activatedPreview = false; + enumerateDevices().then(gotDevices2).then(function() { + grabAudio("videosource", "#audioSource3", null, new_constraints); + }); + } -function updateAudioConstraints(constraint, value=null){ // this is what it SHOULD be, but this doesn't work yet. +function updateAudioConstraints(constraint, value = null) { // this is what it SHOULD be, but this doesn't work yet. var track0 = session.streamSrc.getAudioTracks(); track0 = track0[0]; - if (value == parseFloat(value)){ + if (value == parseFloat(value)) { value = parseFloat(value); - } else if (value == "true"){ + } else if (value == "true") { value = true; - } else if (value == "false"){ + } else if (value == "false") { value = false; } - log({advanced: [ {[constraint]: value} ]}); - track0.applyConstraints({advanced: [ {[constraint]: value} ]}); + log({ + advanced: [{ + [constraint]: value + }] + }); + track0.applyConstraints({ + advanced: [{ + [constraint]: value + }] + }); return; - + } var originalBitrate = session.totalRoomBitrate; -function listCameraSettings(){ +function listCameraSettings() { getById("popupSelector_constraints_video").innerHTML = ""; - - if ((originalBitrate) && (session.roomid) && (session.view!=="") && (session.controlRoomBitrate)){ + + if ((originalBitrate) && (session.roomid) && (session.view !== "") && (session.controlRoomBitrate)) { log("LISTING OPTION FOR BITRATE CONTROL"); var i = "room video bitrate (kbps)"; var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerHTML = i+":"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = i + ":"; label.title = "If you're on a slow network, you can improve frame rate and audio quality by reducing the amount of video data that others send you"; - + var input = document.createElement("input"); input.min = 0; input.max = parseInt(originalBitrate); - - if (getById("popupSelector_constraints_video").style.display == "none"){ - getById("advancedOptionsCamera").style.display="inline-block"; + + if (getById("popupSelector_constraints_video").style.display == "none") { + getById("advancedOptionsCamera").style.display = "inline-block"; } - + input.value = session.totalRoomBitrate; - label.innerHTML = i+": "+session.totalRoomBitrate; - + label.innerHTML = i + ": " + session.totalRoomBitrate; + input.type = "range"; input.dataset.keyname = i; - input.id = "constraints_"+i; - input.style="display:block; width:100%;"; - input.name = "constraints_"+i; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; input.title = "If you're on a slow network, you can improve frame rate and audio quality by reducing the amount of video data that others send you"; - - - input.onchange = function(e){ - getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; - - if (e.target.value>originalBitrate){return;} - else {session.totalRoomBitrate = parseInt(e.target.value);} + + + input.onchange = function(e) { + getById("label_" + e.target.dataset.keyname).innerHTML = e.target.dataset.keyname + ": " + e.target.value; + + if (e.target.value > originalBitrate) { + return; + } else { + session.totalRoomBitrate = parseInt(e.target.value); + } updateMixer(); }; - - + + getById("popupSelector_constraints_video").appendChild(label); getById("popupSelector_constraints_video").appendChild(input); - + } try { var track0 = session.streamSrc.getVideoTracks(); - if (track0.length){ + if (track0.length) { track0 = track0[0]; - if (track0.getCapabilities){ + if (track0.getCapabilities) { session.cameraConstraints = track0.getCapabilities(); } log(session.cameraConstraints); } - } catch(e){ + } catch (e) { errorlog(e); return; } - + try { - - if (track0.getSettings){ + + if (track0.getSettings) { session.currentCameraConstraints = track0.getSettings(); } - } catch(e){ + } catch (e) { errorlog(e); } - - for (var i in session.cameraConstraints){ + + for (var i in session.cameraConstraints) { try { log(i); log(session.cameraConstraints[i]); - if ((typeof session.cameraConstraints[i] ==='object') && (session.cameraConstraints[i] !== null) && ("max" in session.cameraConstraints[i]) && ("min" in session.cameraConstraints[i])){ - if (i==="aspectRatio"){continue;} - else if (i==="width"){continue;} - else if (i==="height"){continue;} - else if (i==="frameRate"){continue;} - - - + if ((typeof session.cameraConstraints[i] === 'object') && (session.cameraConstraints[i] !== null) && ("max" in session.cameraConstraints[i]) && ("min" in session.cameraConstraints[i])) { + if (i === "aspectRatio") { + continue; + } else if (i === "width") { + continue; + } else if (i === "height") { + continue; + } else if (i === "frameRate") { + continue; + } + + var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerHTML = i+":"; - + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = i + ":"; + var input = document.createElement("input"); input.min = session.cameraConstraints[i].min; input.max = session.cameraConstraints[i].max; - - if (parseFloat(input.min) == parseFloat(input.max)){continue;} - - if (getById("popupSelector_constraints_video").style.display == "none"){ - getById("advancedOptionsCamera").style.display="inline-block"; + + if (parseFloat(input.min) == parseFloat(input.max)) { + continue; } - - if (i in session.currentCameraConstraints){ + + if (getById("popupSelector_constraints_video").style.display == "none") { + getById("advancedOptionsCamera").style.display = "inline-block"; + } + + if (i in session.currentCameraConstraints) { input.value = session.currentCameraConstraints[i]; - label.innerHTML = i+": "+session.currentCameraConstraints[i]; + label.innerHTML = i + ": " + session.currentCameraConstraints[i]; + label.title = "Previously was: " + session.currentCameraConstraints[i]; + input.title = "Previously was: " + session.currentCameraConstraints[i]; } else { label.innerHTML = i; } - if ("step" in session.cameraConstraints[i]){ + if ("step" in session.cameraConstraints[i]) { input.step = session.cameraConstraints[i].step; } input.type = "range"; input.dataset.keyname = i; - input.id = "constraints_"+i; - input.style="display:block; width:100%;"; - input.name = "constraints_"+i; - - - input.onchange = function(e){ - getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + + input.onchange = function(e) { + getById("label_" + e.target.dataset.keyname).innerHTML = e.target.dataset.keyname + ": " + e.target.value; updateCameraConstraints(e.target.dataset.keyname, e.target.value); }; - - + + getById("popupSelector_constraints_video").appendChild(label); getById("popupSelector_constraints_video").appendChild(input); - } else if ((typeof session.cameraConstraints[i] ==='object') && (session.cameraConstraints[i] !== null)){ - if (i == "resizeMode"){continue;} - + } else if ((typeof session.cameraConstraints[i] === 'object') && (session.cameraConstraints[i] !== null)) { + if (i == "resizeMode") { + continue; + } + var div = document.createElement("div"); var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerHTML = i+":"; - label.style="display:inline-block; padding:0;margin: 15px 0px 29px;"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = i + ":"; + label.style = "display:inline-block; padding:0;margin: 15px 0px 29px;"; label.dataset.keyname = i; var input = document.createElement("select"); var c = document.createElement("option"); - - if (session.cameraConstraints[i].length>1){ + + if (session.cameraConstraints[i].length > 1) { for (var opts in session.cameraConstraints[i]) { - log(opts); - var opt = new Option(session.cameraConstraints[i][opts], session.cameraConstraints[i][opts]); - input.options.add(opt); - if (i in session.currentCameraConstraints){ - if (session.cameraConstraints[i][opts] == session.currentCameraConstraints[i]){ - opt.selected=true; + log(opts); + var opt = new Option(session.cameraConstraints[i][opts], session.cameraConstraints[i][opts]); + input.options.add(opt); + if (i in session.currentCameraConstraints) { + if (session.cameraConstraints[i][opts] == session.currentCameraConstraints[i]) { + opt.selected = true; + } } - } } - } else if (i.toLowerCase == "torch"){ + } else if (i.toLowerCase == "torch") { var opt = new Option("Off", false); input.options.add(opt); opt = new Option("On", true); @@ -7718,17 +9032,17 @@ function listCameraSettings(){ } else { continue; } - - if (getById("popupSelector_constraints_video").style.display == "none"){ - getById("advancedOptionsCamera").style.display="inline-block"; + + if (getById("popupSelector_constraints_video").style.display == "none") { + getById("advancedOptionsCamera").style.display = "inline-block"; } - - input.id = "constraints_"+i; - input.className="constraintCameraInput"; - input.name = "constraints_"+i; + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; input.style = "display:inline; padding:2px; margin:0 10px;"; input.dataset.keyname = i; - input.onchange = function(e){ + input.onchange = function(e) { //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; updateCameraConstraints(e.target.dataset.keyname, e.target.value); log(e.target.dataset.keyname, e.target.value); @@ -7736,33 +9050,33 @@ function listCameraSettings(){ getById("popupSelector_constraints_video").appendChild(div); div.appendChild(label); div.appendChild(input); - } else if (typeof session.cameraConstraints[i] === 'boolean'){ - + } else if (typeof session.cameraConstraints[i] === 'boolean') { + var div = document.createElement("div"); var label = document.createElement("label"); - label.id= "label_"+i; - label.htmlFor = "constraints_"+i; - label.innerHTML = i+":"; - label.style="display:inline-block; padding:0;margin: 15px 0px 29px;"; + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = i + ":"; + label.style = "display:inline-block; padding:0;margin: 15px 0px 29px;"; label.dataset.keyname = i; var input = document.createElement("select"); var c = document.createElement("option"); - + var opt = new Option("Off", false); input.options.add(opt); opt = new Option("On", true); input.options.add(opt); - - if (getById("popupSelector_constraints_video").style.display == "none"){ - getById("advancedOptionsCamera").style.display="inline-block"; + + if (getById("popupSelector_constraints_video").style.display == "none") { + getById("advancedOptionsCamera").style.display = "inline-block"; } - - input.id = "constraints_"+i; - input.className="constraintCameraInput"; - input.name = "constraints_"+i; + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; input.style = "display:inline; padding:2px; margin:0 10px;"; input.dataset.keyname = i; - input.onchange = function(e){ + input.onchange = function(e) { //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; updateCameraConstraints(e.target.dataset.keyname, e.target.value); log(e.target.dataset.keyname, e.target.value); @@ -7771,31 +9085,41 @@ function listCameraSettings(){ div.appendChild(label); div.appendChild(input); } - } catch(e){errorlog(e);} - + } catch (e) { + errorlog(e); + } + } } - -function updateCameraConstraints(constraint, value=null){ + +function updateCameraConstraints(constraint, value = null) { var track0 = session.streamSrc.getVideoTracks(); track0 = track0[0]; - if (value == parseFloat(value)){ + if (value == parseFloat(value)) { value = parseFloat(value); - } else if (value == "true"){ + } else if (value == "true") { value = true; - } else if (value == "false"){ + } else if (value == "false") { value = false; } - log({advanced: [ {[constraint]: value} ]}); - track0.applyConstraints({advanced: [ {[constraint]: value} ]}); + log({ + advanced: [{ + [constraint]: value + }] + }); + track0.applyConstraints({ + advanced: [{ + [constraint]: value + }] + }); return; - + } - -function setupWebcamSelection(stream=null){ + +function setupWebcamSelection(stream = null) { log("setup webcam"); - - if (stream){ + + if (stream) { log(getById("previewWebcam")); session.streamSrc = stream; getById("previewWebcam").srcObject = outboundAudioPipeline(session.streamSrc); @@ -7803,354 +9127,580 @@ function setupWebcamSelection(stream=null){ } else { log("THIS IS NO STREAM??"); } - - if (!session.videoElement){ + + if (!session.videoElement) { session.videoElement = getById("previewWebcam"); } - + try { - return enumerateDevices().then(gotDevices).then(function(){ - - - if (parseInt(getById("webcamquality").elements.namedItem("resolution").value)==3){ - session.maxframerate = 30; + return enumerateDevices().then(gotDevices).then(function() { + + + if (parseInt(getById("webcamquality").elements.namedItem("resolution").value) == 3) { + session.maxframerate = 30; } else { session.maxframerate = false; } - + //if ((iOS) || (iPad)){ - //getById("multiselect1").parentNode.style.visibility="hidden"; - //getById("multiselect1").parentNode.style.height="0px"; - //getById("multiselecta1").parentNode.style.height="0px"; - //getById("multiselecta1").parentNode.style.visibility="hidden"; + //getById("multiselect1").parentNode.style.visibility="hidden"; + //getById("multiselect1").parentNode.style.height="0px"; + //getById("multiselecta1").parentNode.style.height="0px"; + //getById("multiselecta1").parentNode.style.visibility="hidden"; //} - + var audioSelect = document.querySelector('#audioSource'); var videoSelect = document.querySelector('select#videoSourceSelect'); var outputSelect = document.querySelector('select#outputSource'); - - audioSelect.onchange = function(){ - + + audioSelect.onchange = function() { + var gowebcam = getById("gowebcam"); - if (gowebcam){ + if (gowebcam) { gowebcam.disabled = true; gowebcam.dataset.ready = "true"; gowebcam.style.backgroundColor = "#DDDDDD"; - gowebcam.style.fontWeight="normal"; + gowebcam.style.fontWeight = "normal"; gowebcam.innerHTML = "Waiting for Camera to load"; - miniTranslate(gowebcam,"waiting-for-camera-to-load"); + miniTranslate(gowebcam, "waiting-for-camera-to-load"); } - activatedPreview=false; + activatedPreview = false; grabAudio(); }; - videoSelect.onchange = function(){ - + videoSelect.onchange = function() { + var gowebcam = getById("gowebcam"); - if(gowebcam){ + if (gowebcam) { gowebcam.disabled = true; gowebcam.dataset.ready = "true"; gowebcam.style.backgroundColor = "#DDDDDD"; - gowebcam.style.fontWeight="normal"; + gowebcam.style.fontWeight = "normal"; gowebcam.innerHTML = "Waiting for Camera to load"; - miniTranslate(gowebcam,"waiting-for-camera-to-load"); + miniTranslate(gowebcam, "waiting-for-camera-to-load"); } warnlog("video source changed"); - - activatedPreview=false; - if (session.quality!==false){ + + activatedPreview = false; + if (session.quality !== false) { grabVideo(session.quality); } else { session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); grabVideo(session.quality_wb); } }; - - outputSelect.onchange = function(){ - - if ((iOS) || (iPad)){ + + outputSelect.onchange = function() { + + if ((iOS) || (iPad)) { return; } - + session.sink = outputSelect.options[outputSelect.selectedIndex].value; //if (session.sink=="default"){session.sink=false;} else { - getById("previewWebcam").setSinkId(session.sink).then(() => { - log("New Output Device:"+session.sink); - }).catch(error => { - errorlog("6597"); - errorlog(error); - //setTimeout(function(){alert("Failed to change audio output destination.");},1); - }); + getById("previewWebcam").setSinkId(session.sink).then(() => { + log("New Output Device:" + session.sink); + }).catch(error => { + errorlog("6597"); + errorlog(error); + //setTimeout(function(){alert("Failed to change audio output destination.");},1); + }); //} } - - getById("webcamquality").onchange = function(){ + + getById("webcamquality").onchange = function() { var gowebcam = getById("gowebcam"); - if (gowebcam){ + if (gowebcam) { gowebcam.disabled = true; gowebcam.dataset.ready = "true"; gowebcam.style.backgroundColor = "#DDDDDD"; - gowebcam.style.fontWeight="normal"; + gowebcam.style.fontWeight = "normal"; gowebcam.innerHTML = "Waiting for Camera to load"; - miniTranslate(gowebcam,"waiting-for-camera-to-load"); + miniTranslate(gowebcam, "waiting-for-camera-to-load"); } - - if (parseInt(getById("webcamquality").elements.namedItem("resolution").value)==3){ - session.maxframerate = 30; + + if (parseInt(getById("webcamquality").elements.namedItem("resolution").value) == 3) { + session.maxframerate = 30; } else { session.maxframerate = false; } - activatedPreview=false; + activatedPreview = false; session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); grabVideo(session.quality_wb); }; - if ((session.audioDevice) && (session.audioDevice!==1)){ // change from Auto to Selected Audio Device + if ((session.audioDevice) && (session.audioDevice !== 1)) { // change from Auto to Selected Audio Device log("SETTING AUDIO DEVICE!!"); - activatedPreview=false; + activatedPreview = false; grabAudio(); } - if (session.videoDevice===0){ - if (session.autostart){ - publishWebcam(); // no need to mirror as there is no video... + if (session.videoDevice === 0) { + if (session.autostart) { + publishWebcam(); // no need to mirror as there is no video... return; } else { var gowebcam = getById("gowebcam"); - if (gowebcam){ - gowebcam.disabled =false; + if (gowebcam) { + gowebcam.disabled = false; gowebcam.dataset.ready = "true"; gowebcam.style.backgroundColor = "#3C3"; gowebcam.style.color = "black"; - gowebcam.style.fontWeight="bold"; + gowebcam.style.fontWeight = "bold"; gowebcam.innerHTML = "START"; - miniTranslate(gowebcam,"start"); + miniTranslate(gowebcam, "start"); } return; } } else { - log("GRabbing video: "+session.quality); + log("GRabbing video: " + session.quality); activatedPreview = false; - if (session.quality!==false){ + if (session.quality !== false) { grabVideo(session.quality); } else { session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); grabVideo(session.quality_wb); } } - - if ((iOS) || (iPad)){ - + + if ((iOS) || (iPad)) { + return; } - if (outputSelect.selectedIndex>=0){ + if (outputSelect.selectedIndex >= 0) { session.sink = outputSelect.options[outputSelect.selectedIndex].value; } - if (document.getElementById("previewWebcam") && document.getElementById("previewWebcam").setSinkId){ - if (session.sink){ - getById("previewWebcam").setSinkId(session.sink).then(() => { - }).catch(error => { + if (document.getElementById("previewWebcam") && document.getElementById("previewWebcam").setSinkId) { + if (session.sink) { + getById("previewWebcam").setSinkId(session.sink).then(() => {}).catch(error => { errorlog("6665"); errorlog(error); }); } } - - }).catch(e => {errorlog(e);}) - } catch (e){errorlog(e);} + + }).catch(e => { + errorlog(e); + }) + } catch (e) { + errorlog(e); + } } -Promise.wait = function (ms) { - return new Promise(function (resolve) { - setTimeout(resolve, ms); - }); +Promise.wait = function(ms) { + return new Promise(function(resolve) { + setTimeout(resolve, ms); + }); }; Promise.prototype.timeout = function(ms) { - return Promise.race([ - this, - Promise.wait(ms).then(function () { + return Promise.race([ + this + , Promise.wait(ms).then(function() { var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling it.\n\nPlease also ensure your camera and audio device are correctly connected and not already in use. You may also need to refresh the page."); errormsg.name = "timedOut"; errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling it.\n\nPlease also ensure your camera and audio device are correctly connected and not already in use. You may also need to refresh the page." throw errormsg; - - }) - ]) + + }) + ]) }; -function previewWebcam(){ - - - if (session.taintedSession===null){ - log("STILL WAITING ON HASH TO VALIDATE") - setTimeout(function(){previewWebcam();},1000); +function createIframePopup() { + + if (session.screenShareElement) { + session.screenShareElement.contentWindow.postMessage({ + "close": true + }, '*'); + session.screenShareElement.parentNode.removeChild(session.screenShareElement); + session.screenShareElement = false; + updateMixer(); + getById("screenshare2button").classList.add("float"); + getById("screenshare2button").classList.remove("float2"); return; - } else if (session.taintedSession===true){ + } + + //var iframeContainer = document.createElement("div"); + //iframeContainer.className="popup"; + //iframeContainer.style.zIndex = ""+22; + //iframeContainer.style.width="640px"; + //iframeContainer.style.height="480px"; + //iframeContainer.style.position = "fixed"; + //iframeContainer.style.top = "60px"; + //iframeContainer.style.left = "0"; + //iframeContainer.style.border = "dotted 3px #777"; + //iframeContainer.style.overflow="auto"; + //iframeContainer.style.resize="both"; + //session.screenShareElement = iframeContainer; + //session.screenShareElement.dataset.doNotMove = true; + + //var button1 = document.createElement("button"); + //button1.innerHTML = " Move"; + //button1.className = "popup-header menu"; + //iframeContainer.appendChild(button1); + + //var button2 = document.createElement("button"); + //button2.innerHTML = " Close"; + //button2.className = "menu"; + //button2.onclick = function(){ + // iframe.contentWindow.postMessage({"close":true}, '*'); + // iframe.parentNode.parentNode.removeChild(iframeContainer); + // session.screenShareElement = false; + //} + //iframeContainer.appendChild(button2); + + //var button3 = document.createElement("button"); + //button3.innerHTML = " Reload"; + //button3.className = "menu reload"; + //button3.onclick = function(){iframe.contentWindow.postMessage({"reload":true}, '*');} + //iframeContainer.appendChild(button3); + + if (session.screenshareid) { + var iFrameID = session.screenshareid; + } else { + var iFrameID = session.streamID.substring(0, 12) + "_" + session.generateStreamID(5); + } + + if (session.exclude) { + session.exclude.push(iFrameID); + } else { + session.exclude = []; + session.exclude.push(iFrameID); + } + + var iframe = document.createElement("iframe"); + iframe.allow = "autoplay"; + iframe.allowtransparency = "true"; + var pass = ""; + if (session.password) { + pass = "&password=" + session.password; // encodeURIComponent( + } + iframe.src = "./?screenshare&transparent&cleanoutput&noheader&ad=0&autostart&view&room=" + session.roomid + "&push=" + iFrameID + pass; + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.overflow = "hidden"; + + //iframeContainer.appendChild(iframe); + + + session.screenShareElement = iframe; + session.screenShareElement.dataset.doNotMove = true; + + + document.getElementById("main").appendChild(iframe); + + + updateMixer(); + getById("screenshare2button").classList.add("float2"); + getById("screenshare2button").classList.remove("float"); + + return; // ignore the rest. + + try { + iframeContainer.dragElement = false; + //iframeContainer.style.bottom = "auto"; + button1.style.cursor = "grab"; + button1.stashonmouseup = null; + button1.stashonmousemove = null; + + } catch (e) { + return; + }; + + var pos1 = 0 + , pos2 = 0 + , pos3 = 0 + , pos4 = 0; + button1.onmousedown = dragMouseDown; + + var w = window.innerWidth; + var h = window.innerHeight; + + function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + + pos3 = e.clientX; + pos4 = e.clientY; + button1.stashonmouseup = document.onmouseup; // I don't want to interfere with other drag events. + button1.stashonmousemove = document.onmousemove; + + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + + e = e || window.event; + e.preventDefault(); + + try { + if (("buttons" in e) && e.buttons !== 1) { + closeDragElement(); + //document.onmouseup = null; + //document.onmousemove = null; + return; + } + } catch (e) {} + + iframeContainer.dragElement = true; + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + + + //log(button1.offsetTop +" "+ button1.offsetLeft +" "+ button1.offsetHeight +" "+ button1.offsetWidth +" "+ pos2+" "+pos1 ); + //if (Math.abs(pos1)>350){ + // closeDragElement() + // return; + //} else if (Math.abs(pos2)>350){ + // closeDragElement() + // return; + //} + ///////// + + var topDrag = (iframeContainer.offsetTop - pos2); + var botDrag = window.innerHeight - (iframeContainer.offsetTop + 40); + + if (topDrag < -15) { + closeDragElement(); + topDrag = -5 + }; + if (botDrag < -10) { + closeDragElement(); + topDrag = window.innerHeight - 40 + }; + + ///// + + var leftDrag = (iframeContainer.offsetLeft - pos1); + var rightDrag = window.innerWidth - (iframeContainer.offsetLeft + button1.offsetWidth); + + //log(rightDrag); + + if (leftDrag < -10) { + closeDragElement(); + leftDrag = 0 + }; + if (rightDrag < -10) { + closeDragElement(); + leftDrag = window.innerWidth - button1.offsetWidth + }; + + /////////// + + iframeContainer.style.top = topDrag + "px"; + iframeContainer.style.left = (leftDrag) + "px"; + + } + + function closeDragElement() { + document.onmouseup = null; + document.onmousemove = null; + } +} + +function previewWebcam() { + + + if (session.taintedSession === null) { + log("STILL WAITING ON HASH TO VALIDATE") + setTimeout(function() { + previewWebcam(); + }, 1000); + return; + } else if (session.taintedSession === true) { warnlog("HASH FAILED; PASSWORD NOT VALID"); return; } else { log("NOT TAINTED"); } - - if( activatedPreview == true){ + + if (activatedPreview == true) { log("activeated preview return 1"); return; } activatedPreview = true; - - if (session.audioDevice===0){ // OFF - var constraint = {audio: false}; - } else if ((session.echoCancellation !== false) && (session.autoGainControl !== false) && (session.noiseSuppression !== false)){ // AUTO - var constraint = {audio: true}; - } else { // Disable Echo Cancellation and stuff for the PREVIEW (DEFAULT CAM/MIC) - var constraint = {audio: {}}; - if (session.echoCancellation !== false){ // if not disabled, we assume it's on + + if (session.audioDevice === 0) { // OFF + var constraint = { + audio: false + }; + } else if ((session.echoCancellation !== false) && (session.autoGainControl !== false) && (session.noiseSuppression !== false)) { // AUTO + var constraint = { + audio: true + }; + } else { // Disable Echo Cancellation and stuff for the PREVIEW (DEFAULT CAM/MIC) + var constraint = { + audio: {} + }; + if (session.echoCancellation !== false) { // if not disabled, we assume it's on constraint.audio.echoCancellation = true; } else { constraint.audio.echoCancellation = false; } - if (session.autoGainControl !== false){ + if (session.autoGainControl !== false) { constraint.audio.autoGainControl = true; } else { constraint.audio.autoGainControl = false; } - if (session.noiseSuppression !== false){ + if (session.noiseSuppression !== false) { constraint.audio.noiseSuppression = true; } else { constraint.audio.noiseSuppression = false; } } - - if (session.videoDevice===0){ + + if (session.videoDevice === 0) { constraint.video = false; } else { constraint.video = true; } - -// try { -// var autoPlayAllowed = false; -// log("trying to play video"); -// -// var vid = document.createElement('video'); -// vid.src = ""; // we need this.play(); -// playPromise = vid.play(); -// // In browsers that don’t yet support this functionality, -// // playPromise won’t be defined. -// if (playPromise !== undefined) { -// getById("getPermissions").style.display=""; -// getById("gowebcam").style.display="none"; -// //activatedPreview=false; -// log("checking promise"); -// log(playPromise); -// playPromise.catch(function(error) { -// if (error.name !== 'NotAllowedError') { -// autoPlayAllowed=true; -// log("PROMISE GOOD"); -// } -// }).finally(function() { -// log("FINALLY "); -// delete vid; -// if (autoPlayAllowed) { /////// GOOD ////// -// log("ALLOWED TO PLAY"); -// getById("gowebcam").style.display=""; -// getById("getPermissions").style.display="none"; - enumerateDevices().then(function(devices){ - log("enumeratated"); - log(devices); - var vtrue = false; - var atrue = false; - devices.forEach(function(device) { - if (device.kind === 'audioinput'){ - atrue=true; - } else if (device.kind === 'videoinput'){ - vtrue = true; - } - }); - if (atrue===false){ - constraint.audio = false; - } - if (vtrue===false){ - constraint.video = false; - } - setTimeout(function(constraint){requestBasicPermissions(constraint);},0,constraint); - }).catch((error)=>{ - log("enumeratated failed. Seeking permissions."); - setTimeout(function(constraint){requestBasicPermissions(constraint);},0,constraint); - }); -// return; -// -// } else { -// // BUTTON. -// log("NOT ALLOWED TO CONTINUE -"); -// } -// }); -// } else { -// delete vid; -// enumerateDevices().then(function(devices){ -// log("enumeratated"); -// log(devices); -// var vtrue = false; -// var atrue = false; -// devices.forEach(function(device) { -// if (device.kind === 'audioinput'){ -// atrue=true; -// } else if (device.kind === 'videoinput'){ -// vtrue = true; -// } -// }); -// if (atrue===false){ -// constraint.audio = false; -// } -// if (vtrue===false){ -// constraint.video = false; -// } -// setTimeout(function(constraint){requestBasicPermissions(constraint);},0,constraint); -// }).catch((error)=>{ -// log("enumeratated failed. Seeking permissions."); -// setTimeout(function(constraint){requestBasicPermissions(constraint);},0,constraint); -// }); -// } -// -// } catch(e){ -// setTimeout(function(constraint){requestBasicPermissions(constraint);},0,constraint); -// errorlog(e); -// } + + // try { + // var autoPlayAllowed = false; + // log("trying to play video"); + // + // var vid = document.createElement('video'); + // vid.src = ""; // we need this.play(); + // playPromise = vid.play(); + // // In browsers that don’t yet support this functionality, + // // playPromise won’t be defined. + // if (playPromise !== undefined) { + // getById("getPermissions").style.display=""; + // getById("gowebcam").style.display="none"; + // //activatedPreview=false; + // log("checking promise"); + // log(playPromise); + // playPromise.catch(function(error) { + // if (error.name !== 'NotAllowedError') { + // autoPlayAllowed=true; + // log("PROMISE GOOD"); + // } + // }).finally(function() { + // log("FINALLY "); + // delete vid; + // if (autoPlayAllowed) { /////// GOOD ////// + // log("ALLOWED TO PLAY"); + // getById("gowebcam").style.display=""; + // getById("getPermissions").style.display="none"; + enumerateDevices().then(function(devices) { + log("enumeratated"); + log(devices); + var vtrue = false; + var atrue = false; + devices.forEach(function(device) { + if (device.kind === 'audioinput') { + atrue = true; + } else if (device.kind === 'videoinput') { + vtrue = true; + } + }); + if (atrue === false) { + constraint.audio = false; + } + if (vtrue === false) { + constraint.video = false; + } + setTimeout(function(constraint) { + requestBasicPermissions(constraint); + }, 0, constraint); + }).catch((error) => { + log("enumeratated failed. Seeking permissions."); + setTimeout(function(constraint) { + requestBasicPermissions(constraint); + }, 0, constraint); + }); + // return; + // + // } else { + // // BUTTON. + // log("NOT ALLOWED TO CONTINUE -"); + // } + // }); + // } else { + // delete vid; + // enumerateDevices().then(function(devices){ + // log("enumeratated"); + // log(devices); + // var vtrue = false; + // var atrue = false; + // devices.forEach(function(device) { + // if (device.kind === 'audioinput'){ + // atrue=true; + // } else if (device.kind === 'videoinput'){ + // vtrue = true; + // } + // }); + // if (atrue===false){ + // constraint.audio = false; + // } + // if (vtrue===false){ + // constraint.video = false; + // } + // setTimeout(function(constraint){requestBasicPermissions(constraint);},0,constraint); + // }).catch((error)=>{ + // log("enumeratated failed. Seeking permissions."); + // setTimeout(function(constraint){requestBasicPermissions(constraint);},0,constraint); + // }); + // } + // + // } catch(e){ + // setTimeout(function(constraint){requestBasicPermissions(constraint);},0,constraint); + // errorlog(e); + // } } -function requestBasicPermissions(constraint={video:true,audio:true}){ - if (session.taintedSession===null){ +function requestBasicPermissions(constraint = { + video: true + , audio: true +}) { + if (session.taintedSession === null) { log("STILL WAITING ON HASH TO VALIDATE") - setTimeout(function(constraint){requestBasicPermissions(constraint);},1000,constraint); + setTimeout(function(constraint) { + requestBasicPermissions(constraint); + }, 1000, constraint); return; - } else if (session.taintedSession===true){ + } else if (session.taintedSession === true) { warnlog("HASH FAILED; PASSWORD NOT VALID"); return; } else { log("NOT TAINTED 1"); } - setTimeout(function(){ - getById("getPermissions").style.display="none"; - getById("gowebcam").style.display=""; - },0); + setTimeout(function() { + getById("getPermissions").style.display = "none"; + getById("gowebcam").style.display = ""; + }, 0); log("REQUESTING BASIC PERMISSIONS"); - + try { - var timerBasicCheck=null; - if (!(session.cleanOutput)){ - log("Setting Timer for getUserMedia"); - timerBasicCheck = setTimeout(function(){ - if (!(session.cleanOutput)){ - alert("Camera Access Request Timed Out\nDid you accept camera permissions? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling NDI tools.\n\nPlease also ensure that your camera and audio devices are correctly connected and not already in use. You may also need to refresh the page."); - }},10000); - } - log(constraint); - navigator.mediaDevices.getUserMedia(constraint).then(function(stream){ // Apple needs thi to happen before I can access EnumerateDevices. - log("got first stream"); - clearTimeout(timerBasicCheck); - - setupWebcamSelection(stream); - }).catch(function(err){ - clearTimeout(timerBasicCheck); + var timerBasicCheck = null; + if (!(session.cleanOutput)) { + log("Setting Timer for getUserMedia"); + timerBasicCheck = setTimeout(function() { + if (!(session.cleanOutput)) { + alert("Camera Access Request Timed Out\nDid you accept camera permissions? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling NDI tools.\n\nPlease also ensure that your camera and audio devices are correctly connected and not already in use. You may also need to refresh the page."); + } + }, 10000); + } + + if (session.audioInputChannels) { + if (constraint.audio === true) { + constraint.audio = {}; + constraint.audio.channelCount = session.audioInputChannels; + } else if (constraint.audio) { + constraint.audio.channelCount = session.audioInputChannels; + } + } + + log("CONSTRAINT"); + log(constraint); + navigator.mediaDevices.getUserMedia(constraint).then(function(stream) { // Apple needs thi to happen before I can access EnumerateDevices. + log("got first stream"); + clearTimeout(timerBasicCheck); + setupWebcamSelection(stream); + }).catch(function(err) { + clearTimeout(timerBasicCheck); warnlog("some error with GetUSERMEDIA"); errorlog(err); /* handle the error */ if (err.name == "NotFoundError" || err.name == "DevicesNotFoundError") { @@ -8161,27 +9711,31 @@ function requestBasicPermissions(constraint={video:true,audio:true}){ //constraints can not be satisfied by avb. devices } else if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") { //permission denied in browser - if (!(session.cleanOutput)){ - setTimeout(function(){alert("Permissions denied. Please ensure you have allowed the mic/camera permissions.");},1); + if (!(session.cleanOutput)) { + setTimeout(function() { + alert("Permissions denied. Please ensure you have allowed the mic/camera permissions."); + }, 1); } return; } else if (err.name == "TypeError" || err.name == "TypeError") { //empty constraints object - } else { + } else { //permission denied in browser - if (!(session.cleanOutput)){ - setTimeout(function(){alert(err);},1); + if (!(session.cleanOutput)) { + setTimeout(function() { + alert(err); + }, 1); } } - errorlog("trying to list webcam again"); - setupWebcamSelection(); - }); - } catch (e){ + errorlog("trying to list webcam again"); + setupWebcamSelection(); + }); + } catch (e) { errorlog(e); - if (!(session.cleanOutput)){ + if (!(session.cleanOutput)) { if (window.isSecureContext) { alert("An error has occured when trying to access the webcam or microphone. The reason is not known."); - } else if ((iOS) || (iPad)){ + } else if ((iOS) || (iPad)) { alert("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported."); } else { alert("Error acessing camera or microphone.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia"); @@ -8197,7 +9751,7 @@ function copyFunction(copyText) { copyText.select(); copyText.setSelectionRange(0, 99999); document.execCommand("copy"); - } catch(e) { + } catch (e) { var dummy = document.createElement('input'); document.body.appendChild(dummy); dummy.value = copyText; @@ -8208,10 +9762,10 @@ function copyFunction(copyText) { } } -function generateQRPage(){ +function generateQRPage() { var pass = sanitizePassword(getById("invite_password").value); - if (pass.length){ - return session.generateHash(pass+session.salt,4).then(function(hash){ + if (pass.length) { + return session.generateHash(pass + session.salt, 4).then(function(hash) { generateQRPageCallback(hash); }); } else { @@ -8219,225 +9773,234 @@ function generateQRPage(){ } } -function updateLink(arg, input){ +function updateLink(arg, input) { log(input.dataset.param); - if (input.checked){ - - getById("director_block_"+arg).dataset.raw += input.dataset.param; - - var string = getById("director_block_"+arg).dataset.raw; - - if (getById("obfuscate_director_"+arg).checked){ + if (input.checked) { + + getById("director_block_" + arg).dataset.raw += input.dataset.param; + + var string = getById("director_block_" + arg).dataset.raw; + + if (getById("obfuscate_director_" + arg).checked) { string = obfuscateURL(string); } - - - getById("director_block_"+arg).href = string; - getById("director_block_"+arg).innerText = string; + + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; } else { - var string = getById("director_block_"+arg).dataset.raw+"&"; - string = string.replace(input.dataset.param+"&", "&"); + var string = getById("director_block_" + arg).dataset.raw + "&"; + string = string.replace(input.dataset.param + "&", "&"); string = string.substring(0, string.length - 1); - getById("director_block_"+arg).dataset.raw = string; - - if (getById("obfuscate_director_"+arg).checked){ + getById("director_block_" + arg).dataset.raw = string; + + if (getById("obfuscate_director_" + arg).checked) { string = obfuscateURL(string); } - - getById("director_block_"+arg).href = string; - getById("director_block_"+arg).innerText = string; + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; } } -function updateLinkInverse(arg, input){ +function updateLinkInverse(arg, input) { log(input.dataset.param); - if (!(input.checked)){ - - getById("director_block_"+arg).dataset.raw += input.dataset.param; - - var string = getById("director_block_"+arg).dataset.raw; - - if (getById("obfuscate_director_"+arg).checked){ + if (!(input.checked)) { + + getById("director_block_" + arg).dataset.raw += input.dataset.param; + + var string = getById("director_block_" + arg).dataset.raw; + + if (getById("obfuscate_director_" + arg).checked) { string = obfuscateURL(string); } - - - getById("director_block_"+arg).href = string; - getById("director_block_"+arg).innerText = string; + + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; } else { - var string = getById("director_block_"+arg).dataset.raw+"&"; - string = string.replace(input.dataset.param+"&", "&"); + var string = getById("director_block_" + arg).dataset.raw + "&"; + string = string.replace(input.dataset.param + "&", "&"); string = string.substring(0, string.length - 1); - getById("director_block_"+arg).dataset.raw = string; - - if (getById("obfuscate_director_"+arg).checked){ + getById("director_block_" + arg).dataset.raw = string; + + if (getById("obfuscate_director_" + arg).checked) { string = obfuscateURL(string); } - - getById("director_block_"+arg).href = string; - getById("director_block_"+arg).innerText = string; + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; } } -function updateLinkScene(arg, input){ - var string = getById("director_block_"+arg).dataset.raw; - - if (input.checked){ - string = changeParam(string, "scene","0"); +function updateLinkScene(arg, input) { + var string = getById("director_block_" + arg).dataset.raw; + + if (input.checked) { + string = changeParam(string, "scene", "0"); } else { - string = changeParam(string, "scene","1"); + string = changeParam(string, "scene", "1"); } - getById("director_block_"+arg).dataset.raw = string; - - if (getById("obfuscate_director_"+arg).checked){ + getById("director_block_" + arg).dataset.raw = string; + + if (getById("obfuscate_director_" + arg).checked) { string = obfuscateURL(string); } - - getById("director_block_"+arg).href = string; - getById("director_block_"+arg).innerText = string; + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; } -function resetGen(){ - getById("gencontent").style.display="block"; - getById("gencontent2").style.display="none"; - getById("gencontent2").className=""; //container-inner - getById("gencontent").className="container-inner"; // - getById("gencontent2").innerHTML =""; - getById("videoname4").focus(); +function resetGen() { + getById("gencontent").style.display = "block"; + getById("gencontent2").style.display = "none"; + getById("gencontent2").className = ""; //container-inner + getById("gencontent").className = "container-inner"; // + getById("gencontent2").innerHTML = ""; + getById("videoname4").focus(); } -function generateQRPageCallback(hash){ - try{ +function generateQRPageCallback(hash) { + try { var title = getById("videoname4").value; - if (title.length){ - title = title.replace(/[\W]+/g,"_").replace(/_+/g, '_'); // but not what others might get. - title = "&label="+title; + if (title.length) { + title = title.replace(/[\W]+/g, "_").replace(/_+/g, '_'); // but not what others might get. + title = "&label=" + title; } - var sid = session.generateStreamID(); - + var sid = session.generateStreamID(); + var viewstr = ""; var sendstr = ""; - - if (getById("invite_bitrate").checked){ - viewstr+="&bitrate=20000"; + + if (getById("invite_bitrate").checked) { + viewstr += "&bitrate=20000"; } - if (getById("invite_vp9").checked){ - viewstr+="&codec=vp9"; + if (getById("invite_vp9").checked) { + viewstr += "&codec=vp9"; } - if (getById("invite_stereo").checked){ - viewstr+="&stereo"; - sendstr+="&stereo"; + if (getById("invite_stereo").checked) { + viewstr += "&stereo"; + sendstr += "&stereo"; } - if (getById("invite_automic").checked){ - sendstr+="&audiodevice=1"; + if (getById("invite_automic").checked) { + sendstr += "&audiodevice=1"; } - if (getById("invite_hidescreen").checked){ - sendstr+="&webcam"; + if (getById("invite_hidescreen").checked) { + sendstr += "&webcam"; } - - if (getById("invite_remotecontrol").checked){ // + + if (getById("invite_remotecontrol").checked) { // var remote_gen_id = session.generateStreamID(); - sendstr+="&remote="+remote_gen_id; // security - viewstr+="&remote="+remote_gen_id; + sendstr += "&remote=" + remote_gen_id; // security + viewstr += "&remote=" + remote_gen_id; } - - if (getById("invite_joinroom").value.trim().length){ - sendstr+="&room="+getById("invite_joinroom").value.trim(); - viewstr+="&scene&room="+getById("invite_joinroom").value.trim(); + + if (getById("invite_joinroom").value.trim().length) { + sendstr += "&room=" + getById("invite_joinroom").value.trim(); + viewstr += "&scene&room=" + getById("invite_joinroom").value.trim(); } - - if (getById("invite_password").value.trim().length){ - sendstr+="&hash="+hash; - viewstr+="&password="+getById("invite_password").value.trim(); + + if (getById("invite_password").value.trim().length) { + sendstr += "&hash=" + hash; + viewstr += "&password=" + getById("invite_password").value.trim(); } - - - if (getById("invite_group_chat_type").value){ // 0 is default - if (getById("invite_group_chat_type").value==1){ // no video - sendstr+="&novideo"; - } else if (getById("invite_group_chat_type").value==2){ // no view or audio - sendstr+="&view"; + + + if (getById("invite_group_chat_type").value) { // 0 is default + if (getById("invite_group_chat_type").value == 1) { // no video + sendstr += "&novideo"; + } else if (getById("invite_group_chat_type").value == 2) { // no view or audio + sendstr += "&view"; } } - - if (getById("invite_quality").value){ - if (getById("invite_quality").value==0){ - sendstr+="&quality=0"; - } else if (getById("invite_quality").value==1){ - sendstr+="&quality=1"; - } else if (getById("invite_quality").value==2){ - sendstr+="&quality=2"; + + if (getById("invite_quality").value) { + if (getById("invite_quality").value == 0) { + sendstr += "&quality=0"; + } else if (getById("invite_quality").value == 1) { + sendstr += "&quality=1"; + } else if (getById("invite_quality").value == 2) { + sendstr += "&quality=2"; } } - + sendstr = 'https://' + location.host + location.pathname + '?push=' + sid + sendstr + title; - - if (getById("invite_obfuscate").checked){ + + if (getById("invite_obfuscate").checked) { sendstr = obfuscateURL(sendstr); } - - viewstr = 'https://' + location.host+ location.pathname + '?view=' + sid + viewstr + title; - getById("gencontent").style.display="none"; - getById("gencontent").className=""; // - getById("gencontent2").style.display="block"; - getById("gencontent2").className="container-inner"; // + + viewstr = 'https://' + location.host + location.pathname + '?view=' + sid + viewstr + title; + getById("gencontent").style.display = "none"; + getById("gencontent").className = ""; // + getById("gencontent2").style.display = "block"; + getById("gencontent2").className = "container-inner"; // getById("gencontent2").innerHTML = '
    \

    Guest Invite Link:

    \ '+sendstr+'

    \ -

    and don\'t forget the

    OBS Browser Source Link:

    '+viewstr+' \ + style="word-break: break-all;cursor:copy;background-color:#CFC;border: 2px solid black;width:300px;padding:8px;margin:0px;color:#000;" href="' + sendstr + '" >' + sendstr + '

    \ +

    and don\'t forget the

    OBS Browser Source Link:

    ' + viewstr + ' \

    \ - This invite link and OBS ingestion link are reusable. Only one person may use a specific invite at a time.

    '; + This invite link and OBS ingestion link are reusable. Only one person may use a specific invite at a time.

    '; var qrcode = new QRCode(getById("qrcode"), { - width : 300, - height : 300, - colorDark : "#000000", - colorLight : "#FFFFFF", - useSVG: false + width: 300 + , height: 300 + , colorDark: "#000000" + , colorLight: "#FFFFFF" + , useSVG: false }); qrcode.makeCode(sendstr); - setTimeout(function(){getById("qrcode").title="";},100); // i really hate the title overlay that the qrcode function makes + setTimeout(function() { + getById("qrcode").title = ""; + if (getById("qrcode").getElementsByTagName('img').length) { + getById("qrcode").getElementsByTagName('img')[0].style.cursor = "none"; + } + }, 100); // i really hate the title overlay that the qrcode function makes - } catch(e){ + } catch (e) { errorlog(e); } } -if (session.view){ +if (session.view) { getById("main").className = ""; getById("credits").style.display = 'none'; - try{ - if (session.label===false){ - if (document.title==""){ - document.title = "View="+session.view.toString(); + try { + if (session.label === false) { + if (document.title == "") { + document.title = "View=" + session.view.toString(); } else { - document.title += ", View="+session.view.toString(); + document.title += ", View=" + session.view.toString(); } } - } catch(e){errorlog(e);}; + } catch (e) { + errorlog(e); + }; } -function safariVersion(){ +function safariVersion() { try { var ver = navigator.appVersion.split("Version/"); - if (ver.length>1){ + if (ver.length > 1) { ver = ver[1].split(" Safari"); } - if (ver.length>1){ + if (ver.length > 1) { ver = ver[0].split("."); } - if (ver.length>1){ + if (ver.length > 1) { ver = parseInt(ver[0]); } else { ver = 0; } - } catch(e){return 0;} + } catch (e) { + return 0; + } return ver; } -if ((session.view) && (session.roomid===false)){ +if ((session.view) && (session.roomid === false)) { getById("container-4").className = 'column columnfade'; getById("container-3").className = 'column columnfade'; getById("container-2").className = 'column columnfade'; @@ -8455,58 +10018,62 @@ if ((session.view) && (session.roomid===false)){ getById("mainmenu").style.minHeight = "300px"; getById("mainmenu").style.backgroundSize = "100px 100px"; getById("mainmenu").innerHTML = ''; - - setTimeout(function(){ - try{ - if ((session.view) && (!(session.cleanOutput))){ - if (document.getElementById("mainmenu")){ + + setTimeout(function() { + try { + if ((session.view) && (!(session.cleanOutput))) { + if (document.getElementById("mainmenu")) { getById("mainmenu").style.backgroundImage = "url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHdpZHRoPSI0MHB4IiBoZWlnaHQ9IjQwcHgiIHZpZXdCb3g9IjAgMCA0MCA0MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7IiB4PSIwcHgiIHk9IjBweCI+CiAgICA8ZGVmcz4KICAgICAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWwogICAgICAgICAgICBALXdlYmtpdC1rZXlmcmFtZXMgc3BpbiB7CiAgICAgICAgICAgICAgZnJvbSB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoLTM1OWRlZykKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgQGtleWZyYW1lcyBzcGluIHsKICAgICAgICAgICAgICBmcm9tIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKC0zNTlkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICAgIHN2ZyB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybS1vcmlnaW46IDUwJSA1MCU7CiAgICAgICAgICAgICAgICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiAxLjVzIGxpbmVhciBpbmZpbml0ZTsKICAgICAgICAgICAgICAgIC13ZWJraXQtYmFja2ZhY2UtdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICAgICAgICAgICAgYW5pbWF0aW9uOiBzcGluIDEuNXMgbGluZWFyIGluZmluaXRlOwogICAgICAgICAgICB9CiAgICAgICAgXV0+PC9zdHlsZT4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJvdXRlciI+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwwQzIyLjIwNTgsMCAyMy45OTM5LDEuNzg4MTMgMjMuOTkzOSwzLjk5MzlDMjMuOTkzOSw2LjE5OTY4IDIyLjIwNTgsNy45ODc4MSAyMCw3Ljk4NzgxQzE3Ljc5NDIsNy45ODc4MSAxNi4wMDYxLDYuMTk5NjggMTYuMDA2MSwzLjk5MzlDMTYuMDA2MSwxLjc4ODEzIDE3Ljc5NDIsMCAyMCwwWiIgc3R5bGU9ImZpbGw6YmxhY2s7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNNS44NTc4Niw1Ljg1Nzg2QzcuNDE3NTgsNC4yOTgxNSA5Ljk0NjM4LDQuMjk4MTUgMTEuNTA2MSw1Ljg1Nzg2QzEzLjA2NTgsNy40MTc1OCAxMy4wNjU4LDkuOTQ2MzggMTEuNTA2MSwxMS41MDYxQzkuOTQ2MzgsMTMuMDY1OCA3LjQxNzU4LDEzLjA2NTggNS44NTc4NiwxMS41MDYxQzQuMjk4MTUsOS45NDYzOCA0LjI5ODE1LDcuNDE3NTggNS44NTc4Niw1Ljg1Nzg2WiIgc3R5bGU9ImZpbGw6cmdiKDIxMCwyMTAsMjEwKTsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwzMi4wMTIyQzIyLjIwNTgsMzIuMDEyMiAyMy45OTM5LDMzLjgwMDMgMjMuOTkzOSwzNi4wMDYxQzIzLjk5MzksMzguMjExOSAyMi4yMDU4LDQwIDIwLDQwQzE3Ljc5NDIsNDAgMTYuMDA2MSwzOC4yMTE5IDE2LjAwNjEsMzYuMDA2MUMxNi4wMDYxLDMzLjgwMDMgMTcuNzk0MiwzMi4wMTIyIDIwLDMyLjAxMjJaIiBzdHlsZT0iZmlsbDpyZ2IoMTMwLDEzMCwxMzApOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksMjguNDkzOUMzMC4wNTM2LDI2LjkzNDIgMzIuNTgyNCwyNi45MzQyIDM0LjE0MjEsMjguNDkzOUMzNS43MDE5LDMwLjA1MzYgMzUuNzAxOSwzMi41ODI0IDM0LjE0MjEsMzQuMTQyMUMzMi41ODI0LDM1LjcwMTkgMzAuMDUzNiwzNS43MDE5IDI4LjQ5MzksMzQuMTQyMUMyNi45MzQyLDMyLjU4MjQgMjYuOTM0MiwzMC4wNTM2IDI4LjQ5MzksMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxMDEsMTAxLDEwMSk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMy45OTM5LDE2LjAwNjFDNi4xOTk2OCwxNi4wMDYxIDcuOTg3ODEsMTcuNzk0MiA3Ljk4NzgxLDIwQzcuOTg3ODEsMjIuMjA1OCA2LjE5OTY4LDIzLjk5MzkgMy45OTM5LDIzLjk5MzlDMS43ODgxMywyMy45OTM5IDAsMjIuMjA1OCAwLDIwQzAsMTcuNzk0MiAxLjc4ODEzLDE2LjAwNjEgMy45OTM5LDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoMTg3LDE4NywxODcpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTUuODU3ODYsMjguNDkzOUM3LjQxNzU4LDI2LjkzNDIgOS45NDYzOCwyNi45MzQyIDExLjUwNjEsMjguNDkzOUMxMy4wNjU4LDMwLjA1MzYgMTMuMDY1OCwzMi41ODI0IDExLjUwNjEsMzQuMTQyMUM5Ljk0NjM4LDM1LjcwMTkgNy40MTc1OCwzNS43MDE5IDUuODU3ODYsMzQuMTQyMUM0LjI5ODE1LDMyLjU4MjQgNC4yOTgxNSwzMC4wNTM2IDUuODU3ODYsMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxNjQsMTY0LDE2NCk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMzYuMDA2MSwxNi4wMDYxQzM4LjIxMTksMTYuMDA2MSA0MCwxNy43OTQyIDQwLDIwQzQwLDIyLjIwNTggMzguMjExOSwyMy45OTM5IDM2LjAwNjEsMjMuOTkzOUMzMy44MDAzLDIzLjk5MzkgMzIuMDEyMiwyMi4yMDU4IDMyLjAxMjIsMjBDMzIuMDEyMiwxNy43OTQyIDMzLjgwMDMsMTYuMDA2MSAzNi4wMDYxLDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoNzQsNzQsNzQpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksNS44NTc4NkMzMC4wNTM2LDQuMjk4MTUgMzIuNTgyNCw0LjI5ODE1IDM0LjE0MjEsNS44NTc4NkMzNS43MDE5LDcuNDE3NTggMzUuNzAxOSw5Ljk0NjM4IDM0LjE0MjEsMTEuNTA2MUMzMi41ODI0LDEzLjA2NTggMzAuMDUzNiwxMy4wNjU4IDI4LjQ5MzksMTEuNTA2MUMyNi45MzQyLDkuOTQ2MzggMjYuOTM0Miw3LjQxNzU4IDI4LjQ5MzksNS44NTc4NloiIHN0eWxlPSJmaWxsOnJnYig1MCw1MCw1MCk7Ii8+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K')"; getById("mainmenu").innerHTML = '

    Attempting to load video stream.

    '; getById("mainmenu").innerHTML += 'The stream is not available yet or an error occured.

    '; - - }} - } catch(e){ + + } + } + } catch (e) { errorlog("Error handling QR Code failure"); } - },15000); + }, 15000); log("auto playing"); var SafariVer = safariVersion(); - if ((iPad || iOS) && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 && SafariVer>13){ // Modern iOS doesn't need pop up + if ((iPad || iOS) && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 && SafariVer > 13) { // Modern iOS doesn't need pop up play(); - } else if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1){ // Safari on Desktop does require pop up - if (!(session.cleanOutput)){ + } else if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { // Safari on Desktop does require pop up + if (!(session.cleanOutput)) { alert("Safari requires us to ask for an audio permission to use peer-to-peer technology. You will need to accept it in a moment if asked to view this live video"); } - navigator.mediaDevices.getUserMedia({audio: true}).then(function(){ + navigator.mediaDevices.getUserMedia({ + audio: true + }).then(function() { play(); - }).catch(function(){ + }).catch(function() { play(); }); - } else { // everything else is OK. + } else { // everything else is OK. play(); } -} else if (session.roomid){ - try{ - if (session.label===false){ - if (document.title==""){ - document.title = "Room="+session.roomid.toString(); +} else if (session.roomid) { + try { + if (session.label === false) { + if (document.title == "") { + document.title = "Room=" + session.roomid.toString(); } else { - document.title += ", Room="+session.roomid.toString(); + document.title += ", Room=" + session.roomid.toString(); } } - } catch(e){errorlog(e);}; - + } catch (e) { + errorlog(e); + }; + } - -var vis = (function(){ +var vis = (function() { var stateKey, eventKey, keys = { - hidden: "visibilitychange", - webkitHidden: "webkitvisibilitychange", - mozHidden: "mozvisibilitychange", - msHidden: "msvisibilitychange" + hidden: "visibilitychange" + , webkitHidden: "webkitvisibilitychange" + , mozHidden: "mozvisibilitychange" + , msHidden: "msvisibilitychange" }; for (stateKey in keys) { if (stateKey in document) { @@ -8524,233 +10091,58 @@ var vis = (function(){ }; })(); -(function rightclickmenuthing() { // right click menu - "use strict"; - function clickInsideElement( e, className ) { - var el = e.srcElement || e.target; - - if ( el.classList.contains(className) ) { - return el; - } else { - while ( el = el.parentNode ) { - if ( el.classList && el.classList.contains(className) ) { - return el; - } - } - } +(function rightclickmenuthing() { // right click menu + "use strict"; - return false; - } + function clickInsideElement(e, className) { + var el = e.srcElement || e.target; - function getPosition(event2) { - var posx = 0; - var posy = 0; - - if (!event2) var event = window.event; - - if (event2.pageX || event2.pageY) { - posx = event2.pageX; - posy = event2.pageY; - } else if (event2.clientX || event2.clientY) { - posx = event2.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - posy = event2.clientY + document.body.scrollTop + document.documentElement.scrollTop; - } - - return { - x: posx, - y: posy - }; - } - var contextMenuClassName = "context-menu"; - var contextMenuItemClassName = "context-menu__item"; - var contextMenuLinkClassName = "context-menu__link"; - var contextMenuActive = "context-menu--active"; - - var taskItemClassName = "task"; - var taskItemInContext; - - var clickCoords; - var clickCoordsX; - var clickCoordsY; - - var menu = document.querySelector("#context-menu"); - var menuItems = menu.querySelectorAll(".context-menu__item"); - var menuState = 0; - var menuWidth; - var menuHeight; - var menuPosition; - var menuPositionX; - var menuPositionY; - - var windowWidth; - var windowHeight; - - function init() { - contextListener(); - clickListener(); - keyupListener(); - resizeListener(); - } - - function contextListener() { - document.addEventListener( "contextmenu", function(e) { - taskItemInContext = clickInsideElement( e, taskItemClassName ); - - if ( taskItemInContext ) { - e.preventDefault(); - toggleMenuOn(); - positionMenu(e); - } else { - taskItemInContext = null; - toggleMenuOff(); - } - }); - } - - function clickListener() { - document.addEventListener( "click", function(e) { - var clickeElIsLink = clickInsideElement( e, contextMenuLinkClassName ); - - if ( clickeElIsLink ) { - e.preventDefault(); - menuItemListener( clickeElIsLink ); - } else { - var button = e.which || e.button; - if ( button === 1 ) { - toggleMenuOff(); - } - } - }); - } - - function keyupListener() { - // window.onkeyup = function(e) { - // if ( e.keyCode === 27 ) { - // toggleMenuOff(); - // } - // }; - } - - function resizeListener() { - //window.onresize = function(e) { - // toggleMenuOff(); - // }; - } - function toggleMenuOn() { - if ( menuState !== 1 ) { - menuState = 1; - menu.classList.add( contextMenuActive ); - } - } - function toggleMenuOff() { - if ( menuState !== 0 ) { - menuState = 0; - menu.classList.remove( contextMenuActive ); - } - } - function positionMenu(e) { - clickCoords = getPosition(e); - clickCoordsX = clickCoords.x; - clickCoordsY = clickCoords.y; - - menuWidth = menu.offsetWidth + 4; - menuHeight = menu.offsetHeight + 4; - - windowWidth = window.innerWidth; - windowHeight = window.innerHeight; - - if ( (windowWidth - clickCoordsX) < menuWidth ) { - menu.style.left = windowWidth - menuWidth + "px"; - } else { - menu.style.left = clickCoordsX + "px"; - } - - if ( (windowHeight - clickCoordsY) < menuHeight ) { - menu.style.top = windowHeight - menuHeight + "px"; - } else { - menu.style.top = clickCoordsY + "px"; - } - } - function menuItemListener( link ) { - if (link.getAttribute("data-action")=="Open"){ - window.open(taskItemInContext.value); - } else { - // nothing needed - } - log( "Task ID - " + taskItemInContext + ", Task action - " + link.getAttribute("data-action")); - toggleMenuOff(); - } - - init(); - -})(); - -document.addEventListener("dragstart", event => { - var url = event.target.href || event.target.value; - if (!url || !url.startsWith('https://')) return; - if (event.target.dataset.drag!="1"){ - return; - } - //event.target.ondragend = function(){event.target.blur();} - - var streamId = url.split('view='); - var label = url.split('label='); - - if (session.label!==false){ - url += '&layer-name='+session.label; - } else { - url += '&layer-name=OBS.Ninja'; - } - if (streamId.length>1) url += ': ' + streamId[1].split('&')[0]; - if (label.length>1) url += ' - ' + decodeURI(label[1].split('&')[0]); - - try{ - if (document.getElementById("videosource")){ - var video = getById('videosource'); - if (typeof(video.videoWidth) == "undefined"){ - url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += '&layer-height=1080'; - } else if ((parseInt(video.videoWidth)<360) || (video.videoHeight<640)){ - url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += '&layer-height=1080'; - } else { - url += '&layer-width=' + video.videoWidth; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += '&layer-height=' + video.videoHeight; - } + if (el.classList.contains(className)) { + return el; } else { - url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += '&layer-height=1080'; + while (el = el.parentNode) { + if (el.classList && el.classList.contains(className)) { + return el; + } + } } - } catch(error){ - url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += '&layer-height=1080'; + + return false; } - - event.dataTransfer.setDragImage(document.querySelector('#dragImage'), 24, 24); - event.dataTransfer.setData("text/uri-list", encodeURI(url)); - //event.dataTransfer.setData("url", encodeURI(url)); - -}); -function popupMessage(e, message="Copied to Clipboard"){ // right click menu - - var posx = 0; - var posy = 0; - if (!e) var e = window.event; - - if (e.pageX || e.pageY) { - posx = e.pageX; - posy = e.pageY; - } else if (e.clientX || e.clientY) { - posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; - } + function getPosition(event2) { + var posx = 0; + var posy = 0; - posx += 10; + if (!event2) var event = window.event; + if (event2.pageX || event2.pageY) { + posx = event2.pageX; + posy = event2.pageY; + } else if (event2.clientX || event2.clientY) { + posx = event2.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + posy = event2.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } - var menu = document.querySelector("#messagePopup"); - menu.innerHTML = "
    "+message+"
    "; + return { + x: posx + , y: posy + }; + } + var contextMenuClassName = "context-menu"; + var contextMenuItemClassName = "context-menu__item"; + var contextMenuLinkClassName = "context-menu__link"; + var contextMenuActive = "context-menu--active"; + + var taskItemClassName = "task"; + var taskItemInContext; + + var clickCoords; + var clickCoordsX; + var clickCoordsY; + + var menu = document.querySelector("#context-menu"); + var menuItems = menu.querySelectorAll(".context-menu__item"); var menuState = 0; var menuWidth; var menuHeight; @@ -8761,80 +10153,265 @@ function popupMessage(e, message="Copied to Clipboard"){ // right click menu var windowWidth; var windowHeight; - if ( menuState !== 1 ) { - menuState = 1; - menu.classList.add( "context-menu--active" ); + function init() { + contextListener(); + clickListener(); + keyupListener(); + resizeListener(); } - menuWidth = menu.offsetWidth + 4; - menuHeight = menu.offsetHeight + 4; + function contextListener() { + document.addEventListener("contextmenu", function(e) { + taskItemInContext = clickInsideElement(e, taskItemClassName); - windowWidth = window.innerWidth; - windowHeight = window.innerHeight; + if (taskItemInContext) { + e.preventDefault(); + toggleMenuOn(); + positionMenu(e); + } else { + taskItemInContext = null; + toggleMenuOff(); + } + }); + } - if ( (windowWidth - posx) < menuWidth ) { - menu.style.left = windowWidth - menuWidth + "px"; - } else { - menu.style.left = posx + "px"; - } + function clickListener() { + document.addEventListener("click", function(e) { + var clickeElIsLink = clickInsideElement(e, contextMenuLinkClassName); - if ( (windowHeight - posy) < menuHeight ) { - menu.style.top = windowHeight - menuHeight + "px"; - } else { - menu.style.top = posy + "px"; - } - - function toggleMenuOff() { - if ( menuState !== 0 ) { - menuState = 0; - menu.classList.remove( "context-menu--active" ); + if (clickeElIsLink) { + e.preventDefault(); + menuItemListener(clickeElIsLink); + } else { + var button = e.which || e.button; + if (button === 1) { + toggleMenuOff(); + } + } + }); + } + + function keyupListener() { + // window.onkeyup = function(e) { + // if ( e.keyCode === 27 ) { + // toggleMenuOff(); + // } + // }; + } + + function resizeListener() { + //window.onresize = function(e) { + // toggleMenuOff(); + // }; + } + + function toggleMenuOn() { + if (menuState !== 1) { + menuState = 1; + menu.classList.add(contextMenuActive); } } - setTimeout(function(){toggleMenuOff();},1000); + + function toggleMenuOff() { + if (menuState !== 0) { + menuState = 0; + menu.classList.remove(contextMenuActive); + } + } + + function positionMenu(e) { + clickCoords = getPosition(e); + clickCoordsX = clickCoords.x; + clickCoordsY = clickCoords.y; + + menuWidth = menu.offsetWidth + 4; + menuHeight = menu.offsetHeight + 4; + + windowWidth = window.innerWidth; + windowHeight = window.innerHeight; + + if ((windowWidth - clickCoordsX) < menuWidth) { + menu.style.left = windowWidth - menuWidth + "px"; + } else { + menu.style.left = clickCoordsX + "px"; + } + + if ((windowHeight - clickCoordsY) < menuHeight) { + menu.style.top = windowHeight - menuHeight + "px"; + } else { + menu.style.top = clickCoordsY + "px"; + } + } + + function menuItemListener(link) { + if (link.getAttribute("data-action") == "Open") { + window.open(taskItemInContext.value); + } else { + // nothing needed + } + log("Task ID - " + taskItemInContext + ", Task action - " + link.getAttribute("data-action")); + toggleMenuOff(); + } + + init(); + +})(); + +document.addEventListener("dragstart", event => { + var url = event.target.href || event.target.value; + if (!url || !url.startsWith('https://')) return; + if (event.target.dataset.drag != "1") { + return; + } + //event.target.ondragend = function(){event.target.blur();} + + var streamId = url.split('view='); + var label = url.split('label='); + + if (session.label !== false) { + url += '&layer-name=' + session.label; + } else { + url += '&layer-name=OBS.Ninja'; + } + if (streamId.length > 1) url += ': ' + streamId[1].split('&')[0]; + if (label.length > 1) url += ' - ' + decodeURI(label[1].split('&')[0]); + + try { + if (document.getElementById("videosource")) { + var video = getById('videosource'); + if (typeof(video.videoWidth) == "undefined") { + url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += '&layer-height=1080'; + } else if ((parseInt(video.videoWidth) < 360) || (video.videoHeight < 640)) { + url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += '&layer-height=1080'; + } else { + url += '&layer-width=' + video.videoWidth; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += '&layer-height=' + video.videoHeight; + } + } else { + url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += '&layer-height=1080'; + } + } catch (error) { + url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += '&layer-height=1080'; + } + + event.dataTransfer.setDragImage(document.querySelector('#dragImage'), 24, 24); + event.dataTransfer.setData("text/uri-list", encodeURI(url)); + //event.dataTransfer.setData("url", encodeURI(url)); + +}); + +function popupMessage(e, message = "Copied to Clipboard") { // right click menu + + var posx = 0; + var posy = 0; + + if (!e) var e = window.event; + + if (e.pageX || e.pageY) { + posx = e.pageX; + posy = e.pageY; + } else if (e.clientX || e.clientY) { + posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + posx += 10; + + + var menu = document.querySelector("#messagePopup"); + menu.innerHTML = "
    " + message + "
    "; + var menuState = 0; + var menuWidth; + var menuHeight; + var menuPosition; + var menuPositionX; + var menuPositionY; + + var windowWidth; + var windowHeight; + + if (menuState !== 1) { + menuState = 1; + menu.classList.add("context-menu--active"); + } + + menuWidth = menu.offsetWidth + 4; + menuHeight = menu.offsetHeight + 4; + + windowWidth = window.innerWidth; + windowHeight = window.innerHeight; + + if ((windowWidth - posx) < menuWidth) { + menu.style.left = windowWidth - menuWidth + "px"; + } else { + menu.style.left = posx + "px"; + } + + if ((windowHeight - posy) < menuHeight) { + menu.style.top = windowHeight - menuHeight + "px"; + } else { + menu.style.top = posy + "px"; + } + + function toggleMenuOff() { + if (menuState !== 0) { + menuState = 0; + menu.classList.remove("context-menu--active"); + } + } + setTimeout(function() { + toggleMenuOff(); + }, 1000); event.preventDefault(); } function timeSince(date) { - var seconds = Math.floor((new Date() - date) / 1000); + var seconds = Math.floor((new Date() - date) / 1000); - var interval = seconds / 31536000; + var interval = seconds / 31536000; - if (interval > 1) { - return Math.floor(interval) + " years"; - } - interval = seconds / 2592000; - if (interval > 1) { - return Math.floor(interval) + " months"; - } - interval = seconds / 86400; - if (interval > 1) { - return Math.floor(interval) + " days"; - } - interval = seconds / 3600; - if (interval > 1) { - return Math.floor(interval) + " hours"; - } - interval = seconds / 60; - if (interval > 1) { - return Math.floor(interval) + " minutes"; - } - return "Seconds ago"; + if (interval > 1) { + return Math.floor(interval) + " years"; + } + interval = seconds / 2592000; + if (interval > 1) { + return Math.floor(interval) + " months"; + } + interval = seconds / 86400; + if (interval > 1) { + return Math.floor(interval) + " days"; + } + interval = seconds / 3600; + if (interval > 1) { + return Math.floor(interval) + " hours"; + } + interval = seconds / 60; + if (interval > 1) { + return Math.floor(interval) + " minutes"; + } + return "Seconds ago"; } var chatUpdateTimeout = null; var messageList = [] -function sendChatMessage(chatMsg = false){ // filtered + visual +function sendChatMessage(chatMsg = false) { // filtered + visual var data = {}; - if (chatMsg===false){ + if (chatMsg === false) { var msg = document.getElementById('chatInput').value; } else { var msg = chatMsg; } //msg = sanitizeChat(msg); - if (msg==""){return;} + if (msg == "") { + return; + } sendChat(msg); // send message to peers - + data.time = Date.now(); data.msg = sanitizeChat(msg); // this is what the other person should see data.label = false; @@ -8842,58 +10419,62 @@ function sendChatMessage(chatMsg = false){ // filtered + visual document.getElementById('chatInput').value = ""; messageList.push(data); messageList = messageList.slice(-100); - if (session.broadcastChannel!==false){ + if (session.broadcastChannel !== false) { log(session.broadcastChannel); session.broadcastChannel.postMessage(data); } updateMessages(); } -function toggleQualityDirector(bitrate, UUID, ele = null){ // ele is specific to the button in the director's room +function toggleQualityDirector(bitrate, UUID, ele = null) { // ele is specific to the button in the director's room var eles = ele.parentNode.childNodes; - for (i in eles){ - eles[i].className=""; + for (i in eles) { + eles[i].className = ""; } - ele.className="pressed"; + ele.className = "pressed"; session.requestRateLimit(bitrate, UUID); } -function createPopoutChat(){ +function createPopoutChat() { var randid = session.generateStreamID(8); log(randid); - window.open('./popout?id='+randid,'popup','width=600,height=480,toolbar=no,menubar=no,resizable=yes'); + window.open('./popout?id=' + randid, 'popup', 'width=600,height=480,toolbar=no,menubar=no,resizable=yes'); session.broadcastChannel = new BroadcastChannel(randid); - session.broadcastChannel.onmessage = function (e) { - if ("loaded" in e.data){ - session.broadcastChannel.postMessage({messageList:messageList}); - } else if ("msg" in e.data){ + session.broadcastChannel.onmessage = function(e) { + if ("loaded" in e.data) { + session.broadcastChannel.postMessage({ + messageList: messageList + }); + } else if ("msg" in e.data) { sendChatMessage(e.data.msg); } } return false; } -function getChatMessage(msg, label=false, director=false, overlay=false){ - +function getChatMessage(msg, label = false, director = false, overlay = false) { + msg = sanitizeChat(msg); // keep it clean. - if (msg==""){return;} - - if (label){ + if (msg == "") { + return; + } + + if (label) { label = sanitizeLabel(label) } - + data = {}; data.time = Date.now(); data.msg = msg; - if (label){ + if (label) { data.label = label; - if (director){ - data.label = "" + data.label +": "; + if (director) { + data.label = "" + data.label + ": "; } else { - data.label = "" + data.label +": "; + data.label = "" + data.label + ": "; } - } else if (director){ - data.label = "Director: "; + } else if (director) { + data.label = "Director: "; label = "Director"; } else { data.label = ""; @@ -8902,127 +10483,145 @@ function getChatMessage(msg, label=false, director=false, overlay=false){ data.type = "recv"; messageList.push(data); messageList = messageList.slice(-100); + + if (session.beepToNotify) { + playtone(); + } updateMessages(); - - if (overlay && director){ + + if (overlay && director) { var textOverlay = getById("overlayMsgs"); - if (textOverlay){ + if (textOverlay) { var spanOverlay = document.createElement("span"); - spanOverlay.innerHTML = ""+label+": "+msg+"
    "; + spanOverlay.innerHTML = "" + label + ": " + msg + "
    "; textOverlay.appendChild(spanOverlay); - textOverlay.style.display="block"; - var showtime = msg.length*200+3000; - if (showtime>8000){showtime=8000;} - setTimeout(function(ele){ele.parentNode.removeChild(ele);},showtime,spanOverlay); + textOverlay.style.display = "block"; + var showtime = msg.length * 200 + 3000; + if (showtime > 8000) { + showtime = 8000; + } + setTimeout(function(ele) { + ele.parentNode.removeChild(ele); + }, showtime, spanOverlay); } } - - if (session.chat==false){ - getById("chattoggle").className="las la-comments my-float toggleSize puslate"; - getById("chatbutton").className="float"; - - if (getById("chatNotification").value){ - getById("chatNotification").value = getById("chatNotification").value+1; + + if (session.chat == false) { + getById("chattoggle").className = "las la-comments my-float toggleSize puslate"; + getById("chatbutton").className = "float"; + + if (getById("chatNotification").value) { + getById("chatNotification").value = getById("chatNotification").value + 1; } else { getById("chatNotification").value = 1; } getById("chatNotification").classList.add("notification"); } - - if (parent){ - parent.postMessage({"gotChat": data }, "*"); + + if (isIFrame) { + parent.postMessage({ + "gotChat": data + }, "*"); } - if (session.broadcastChannel!==false){ + if (session.broadcastChannel !== false) { session.broadcastChannel.postMessage(data); /* send */ } - + } -function updateClosedCaptions(msg, label, UUID){ +function updateClosedCaptions(msg, label, UUID) { msg.counter = parseInt(msg.counter); var transcript = sanitizeChat(msg.transcript); // keep it clean. - if (transcript==""){return;} + if (transcript == "") { + return; + } transcript = transcript.toUpperCase(); - - if (label){ + + if (label) { label = sanitizeLabel(label); - label = "" + label +": "; + label = "" + label + ": "; } else { label = ""; } - + var textOverlay = getById("overlayMsgs"); - if (textOverlay){ - if (document.getElementById(UUID+"_"+msg.counter)){ - var spanOverlay = document.getElementById(UUID+"_"+msg.counter); + if (textOverlay) { + if (document.getElementById(UUID + "_" + msg.counter)) { + var spanOverlay = document.getElementById(UUID + "_" + msg.counter); } else { var spanOverlay = document.createElement("span"); - spanOverlay.id = UUID+"_"+msg.counter; + spanOverlay.id = UUID + "_" + msg.counter; textOverlay.appendChild(spanOverlay); - textOverlay.style.height = "auto"; + textOverlay.style.height = "auto"; textOverlay.style.textAlign = "left"; - textOverlay.style.display="block"; - textOverlay.style.position="relative"; + textOverlay.style.display = "block"; + textOverlay.style.position = "relative"; } - spanOverlay.innerHTML = label+transcript+"
    "; - - spanOverlay.style.fontSize = (parseInt(session.labelsize || 100)/100.0*4.5)+"vh"; - spanOverlay.style.lineHeight = (parseInt(session.labelsize || 100)/100*6)+"vh"; - spanOverlay.style.margin = (parseInt(session.labelsize || 100)/100.0*0.75)+"vh"; - - if (msg.isFinal){ + spanOverlay.innerHTML = label + transcript + "
    "; + + spanOverlay.style.fontSize = (parseInt(session.labelsize || 100) / 100.0 * 4.5) + "vh"; + spanOverlay.style.lineHeight = (parseInt(session.labelsize || 100) / 100 * 6) + "vh"; + spanOverlay.style.margin = (parseInt(session.labelsize || 100) / 100.0 * 0.75) + "vh"; + + if (msg.isFinal) { var showtime = 3000; clearTimeout(spanOverlay.timeout); - spanOverlay.timeout = setTimeout(function(ele){ele.parentNode.removeChild(ele);},showtime,spanOverlay); + spanOverlay.timeout = setTimeout(function(ele) { + ele.parentNode.removeChild(ele); + }, showtime, spanOverlay); } else { clearTimeout(spanOverlay.timeout); - spanOverlay.timeout = setTimeout(function(ele){ele.parentNode.removeChild(ele);},30000,spanOverlay); + spanOverlay.timeout = setTimeout(function(ele) { + ele.parentNode.removeChild(ele); + }, 30000, spanOverlay); } - + } } -function updateMessages(){ +function updateMessages() { document.getElementById("chatBody").innerHTML = ""; - for (i in messageList){ - + for (i in messageList) { + var time = timeSince(messageList[i].time); var msg = document.createElement("div"); ////// KEEP THIS IN ///////// console.log(messageList[i].msg); // Display Recieved messages for View-Only clients. ///////////////////////////// - if (messageList[i].type == "sent"){ - msg.innerHTML = messageList[i].msg + " - "+time+""; + if (messageList[i].type == "sent") { + msg.innerHTML = messageList[i].msg + " - " + time + ""; msg.classList.add("outMessage"); - } else if (messageList[i].type == "recv"){ + } else if (messageList[i].type == "recv") { var label = ""; - if (messageList[i].label){ + if (messageList[i].label) { label = messageList[i].label; - } - msg.innerHTML = label+messageList[i].msg + " - "+time+""; + } + msg.innerHTML = label + messageList[i].msg + " - " + time + ""; msg.classList.add("inMessage"); - } else if (messageList[i].type == "alert"){ - msg.innerHTML = messageList[i].msg + " - "+time+""; + } else if (messageList[i].type == "alert") { + msg.innerHTML = messageList[i].msg + " - " + time + ""; msg.classList.add("inMessage"); } else { - msg.innerHTML = messageList[i].msg + " - "+time+""; + msg.innerHTML = messageList[i].msg + " - " + time + ""; msg.classList.add("inMessage"); } - + document.getElementById("chatBody").appendChild(msg); } - if (chatUpdateTimeout){ + if (chatUpdateTimeout) { clearInterval(chatUpdateTimeout); } document.getElementById("chatBody").scrollTop = document.getElementById("chatBody").scrollHeight; - chatUpdateTimeout = setTimeout(function(){updateMessages()},60000); + chatUpdateTimeout = setTimeout(function() { + updateMessages() + }, 60000); } -function EnterButtonChat(event){ - // Number 13 is the "Enter" key on the keyboard +function EnterButtonChat(event) { + // Number 13 is the "Enter" key on the keyboard var key = event.which || event.keyCode; - if (key === 13) { + if (key === 13) { // Cancel the default action, if needed event.preventDefault(); // Trigger the button element with a click @@ -9030,103 +10629,102 @@ function EnterButtonChat(event){ } } -function showCustomizer(arg,ele){ +function showCustomizer(arg, ele) { //getById("directorLinksButton").innerHTML=' LINKS (GUEST INVITES & SCENES)' - getById("showCustomizerButton1").style.backgroundColor=""; - getById("showCustomizerButton2").style.backgroundColor=""; - getById("showCustomizerButton3").style.backgroundColor=""; - getById("showCustomizerButton4").style.backgroundColor=""; - getById("showCustomizerButton1").style.boxShadow=""; - getById("showCustomizerButton2").style.boxShadow=""; - getById("showCustomizerButton3").style.boxShadow=""; - getById("showCustomizerButton4").style.boxShadow=""; - - - if (getById("customizeLinks"+arg).style.display!="none"){ - getById("customizeLinks").style.display="none"; - getById("customizeLinks"+arg).style.display="none"; + getById("showCustomizerButton1").style.backgroundColor = ""; + getById("showCustomizerButton2").style.backgroundColor = ""; + getById("showCustomizerButton3").style.backgroundColor = ""; + getById("showCustomizerButton4").style.backgroundColor = ""; + getById("showCustomizerButton1").style.boxShadow = ""; + getById("showCustomizerButton2").style.boxShadow = ""; + getById("showCustomizerButton3").style.boxShadow = ""; + getById("showCustomizerButton4").style.boxShadow = ""; + + + if (getById("customizeLinks" + arg).style.display != "none") { + getById("customizeLinks").style.display = "none"; + getById("customizeLinks" + arg).style.display = "none"; } else { //directorLinks").style.display="none"; - getById("showCustomizerButton"+arg).style.backgroundColor="#1e0000"; - getById("showCustomizerButton"+arg).style.boxShadow="inset 0px 0px 1px #b90000"; - getById("customizeLinks1").style.display="none"; - getById("customizeLinks2").style.display="none"; - getById("customizeLinks3").style.display="none"; - getById("customizeLinks4").style.display="none"; - getById("customizeLinks").style.display="block"; - getById("customizeLinks"+arg).style.display="block"; + getById("showCustomizerButton" + arg).style.backgroundColor = "#1e0000"; + getById("showCustomizerButton" + arg).style.boxShadow = "inset 0px 0px 1px #b90000"; + getById("customizeLinks1").style.display = "none"; + getById("customizeLinks3").style.display = "none"; + getById("customizeLinks").style.display = "block"; + getById("customizeLinks" + arg).style.display = "block"; } } var defaultRecordingBitrate = false; -function recordVideo(target, event, videoKbps = false){ // event.currentTarget,this.parentNode.parentNode.dataset.UUID + +function recordVideo(target, event, videoKbps = false) { // event.currentTarget,this.parentNode.parentNode.dataset.UUID var UUID = target.dataset.UUID; var video = session.rpcs[UUID].videoElement; var audioKbps = false; - - if (event === null){ - if (defaultRecordingBitrate===null){ - updateLocalRecordButton(UUID, -1 ); + + if (event === null) { + if (defaultRecordingBitrate === null) { + updateLocalRecordButton(UUID, -1); //target.style.backgroundColor = null; //target.innerHTML = ' record local'; return; } - } else if ((event.ctrlKey) || (event.metaKey)){ - updateLocalRecordButton(UUID, -3 ); + } else if ((event.ctrlKey) || (event.metaKey)) { + updateLocalRecordButton(UUID, -3); //target.innerHTML = ' ARMED'; //target.style.backgroundColor = "#BF3F3F"; Callbacks.push([recordVideo, target, null, false]); log("Record Video queued"); - defaultRecordingBitrate=false; + defaultRecordingBitrate = false; return; } else { - defaultRecordingBitrate=false; + defaultRecordingBitrate = false; } - + log("Record Video Clicked"); - if ("recording" in video){ - log("ALREADY RECORDING!"); - //target.style.backgroundColor = null; - //target.innerHTML = ' record local'; - updateLocalRecordButton(UUID, -2 ); - video.recorder.stop(); - session.requestRateLimit(35,UUID); // 100kbps - var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="'+UUID+'"]'); - if (elements[0]){ - elements[0].classList.add("pressed"); - } - var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="'+UUID+'"]'); - if (elements[0]){ - elements[0].classList.remove("pressed"); - } - var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="'+UUID+'"]'); - if (elements[0]){ - elements[0].classList.remove("pressed"); - } - return; + if ("recording" in video) { + log("ALREADY RECORDING!"); + //target.style.backgroundColor = null; + //target.innerHTML = ' record local'; + updateLocalRecordButton(UUID, -2); + video.recorder.stop(); + session.requestRateLimit(35, UUID); // 100kbps + var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.add("pressed"); + } + var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.remove("pressed"); + } + var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.remove("pressed"); + } + return; } else { - updateLocalRecordButton(UUID, 0 ); + updateLocalRecordButton(UUID, 0); //target.style.backgroundColor = "#FCC"; //target.innerHTML = " Download"; video.recording = true; } - + video.recorder = {}; - - if (videoKbps==false){ - if (defaultRecordingBitrate==false){ + + if (videoKbps == false) { + if (defaultRecordingBitrate == false) { videoKbps = 4000; // 4mbps recording bitrate - videoKbps = prompt("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)",videoKbps); - if (videoKbps===null){ + videoKbps = prompt("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)", videoKbps); + if (videoKbps === null) { //target.style.backgroundColor = null; //target.innerHTML = ' record local'; - updateLocalRecordButton(UUID, -1 ); + updateLocalRecordButton(UUID, -1); target.style.backgroundColor = ""; delete(video.recorder); delete(video.recording); - defaultRecordingBitrate=null; + defaultRecordingBitrate = null; return; } videoKbps = parseInt(videoKbps); @@ -9134,232 +10732,257 @@ function recordVideo(target, event, videoKbps = false){ // event.currentTarget, } else { videoKbps = defaultRecordingBitrate; } - } - - if (videoKbps<=0){ - audioKbps = videoKbps*(-1); - videoKbps = false; - } else if (videoKbps<50){ // this just makes sure you can't set 0 on the record bitrate. - videoKbps=50; - session.requestRateLimit(parseInt(videoKbps*0.8), UUID); // 3200kbps transfer bitrate. Less than the recording bitrate, to avoid waste. - } else { - session.requestRateLimit(parseInt(videoKbps*0.8), UUID); // 3200kbps transfer bitrate. Less than the recording bitrate, to avoid waste. } - - - var filename = Date.now().toString(); - //var recordedBlobs = []; - + + if (videoKbps <= 0) { + audioKbps = videoKbps * (-1); + videoKbps = false; + } else if (videoKbps < 50) { // this just makes sure you can't set 0 on the record bitrate. + videoKbps = 50; + session.requestRateLimit(parseInt(videoKbps * 0.8), UUID); // 3200kbps transfer bitrate. Less than the recording bitrate, to avoid waste. + } else { + session.requestRateLimit(parseInt(videoKbps * 0.8), UUID); // 3200kbps transfer bitrate. Less than the recording bitrate, to avoid waste. + } + + var timestamp = Date.now(); + var filename = ""; + if (session.rpcs[UUID].label || session.rpcs[UUID].streamID) { + filename = session.rpcs[UUID].label || session.rpcs[UUID].streamID; + filename = filename.replace(/[\W]+/g, "_"); + filename = filename.substring(0, 200); + } + + filename += "_" + timestamp.toString(); + var cancell = false; - if (typeof video.srcObject === "undefined" || !video.srcObject) {return;} - - const { readable, writable } = new TransformStream({ - transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b))) + if (typeof video.srcObject === "undefined" || !video.srcObject) { + return; + } + + const { + readable + , writable + } = new TransformStream({ + transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b))) }); - readable.pipeTo(streamSaver.createWriteStream(filename+'.webm')); + readable.pipeTo(streamSaver.createWriteStream(filename + '.webm')); var writer = writable.getWriter(); video.recorder.writer = writer; - video.recorder.stop = function (){ - if (!video.recording){ + video.recorder.stop = function() { + if (!video.recording) { errorlog("ALREADY STOPPED"); - updateLocalRecordButton(UUID, -1 ); + updateLocalRecordButton(UUID, -1); return; } - video.recording=false; - updateLocalRecordButton(UUID, -2 ); + video.recording = false; + updateLocalRecordButton(UUID, -2); try { - if (video.recorder.mediaRecorder.state!=="inactive"){ + if (video.recorder.mediaRecorder.state !== "inactive") { video.recorder.mediaRecorder.stop(); } - } catch(e){errorlog(e);} - - session.requestRateLimit(35,UUID); // 100kbps - var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="'+UUID+'"]'); - if (elements[0]){ + } catch (e) { + errorlog(e); + } + + session.requestRateLimit(35, UUID); // 100kbps + var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { elements[0].classList.add("pressed"); } - var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="'+UUID+'"]'); - if (elements[0]){ + var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { elements[0].classList.remove("pressed"); } - var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="'+UUID+'"]'); - if (elements[0]){ + var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { elements[0].classList.remove("pressed"); } - + cancell = true; - // log('Recorded Blobs: ', recordedBlobs); + // log('Recorded Blobs: ', recordedBlobs); // download(); setTimeout(() => { writer.close(); - updateLocalRecordButton(UUID, -1 ); + updateLocalRecordButton(UUID, -1); delete(video.recorder); delete(video.recording); - }, 1200); - }; - + }, 1200); + }; + let options = {}; - - if (videoKbps){ - options.mimeType = "video/webm"; - if (videoKbps<1000){ - options.videoBitsPerSecond = parseInt(videoKbps*1024); // 100 kbps audio - } else{ - options.bitsPerSecond = parseInt(videoKbps*1024); // 100 to 132 kbps audio + + if (videoKbps) { + options.mimeType = "video/webm"; + if (videoKbps < 1000) { + options.videoBitsPerSecond = parseInt(videoKbps * 1024); // 100 kbps audio + } else { + options.bitsPerSecond = parseInt(videoKbps * 1024); // 100 to 132 kbps audio } - video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); + video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); } else { options.mimeType = "audio/webm"; - if (audioKbps==0){ - if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")){ - options.mimeType = "audio/webm;codecs=pcm"; + if (audioKbps == 0) { + if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) { + options.mimeType = "audio/webm;codecs=pcm"; } } else { - options.bitsPerSecond = parseInt(audioKbps*1024); + options.bitsPerSecond = parseInt(audioKbps * 1024); } var stream = new MediaStream(); - video.srcObject.getAudioTracks().forEach((track)=>{ + video.srcObject.getAudioTracks().forEach((track) => { stream.addTrack(track, video.srcObject); }); - video.recorder.mediaRecorder = new MediaRecorder(stream, options); + video.recorder.mediaRecorder = new MediaRecorder(stream, options); } log(options); - + function download() { - const blob = new Blob(recordedBlobs, { type: "video/webm" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = filename+".webm"; - document.body.appendChild(a); - a.click(); - setTimeout(() => { - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }, 100); - } - + const blob = new Blob(recordedBlobs, { + type: "video/webm" + }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = filename + ".webm"; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); + } + function handleDataAvailable(event) { - if (event.data && event.data.size > 0) { - //recordedBlobs.push(event.data); + if (event.data && event.data.size > 0) { + //recordedBlobs.push(event.data); writer.write(event.data); //////////// - if (video.recording){ - updateLocalRecordButton(UUID, (parseInt((Date.now() - filename)/1000) || 0) ); + if (video.recording) { + updateLocalRecordButton(UUID, (parseInt((Date.now() - timestamp) / 1000) || 0)); } - } - } - + } + } + video.recorder.mediaRecorder.ondataavailable = handleDataAvailable; - + video.recorder.mediaRecorder.onerror = function(event) { errorlog(event); video.recorder.stop(); - session.requestRateLimit(35,UUID); - if (!(session.cleanOutput)){ - setTimeout(function(){alert("an error occured with the media recorder; stopping recording");},1); + session.requestRateLimit(35, UUID); + if (!(session.cleanOutput)) { + setTimeout(function() { + alert("an error occured with the media recorder; stopping recording"); + }, 1); } }; - - video.srcObject.ended = function(event) { + + video.srcObject.ended = function(event) { video.recorder.stop(); - session.requestRateLimit(35,UUID); - if (!(session.cleanOutput)){ - setTimeout(function(){alert("stream ended! stopping recording");},1); + session.requestRateLimit(35, UUID); + if (!(session.cleanOutput)) { + setTimeout(function() { + alert("stream ended! stopping recording"); + }, 1); } }; - - - setTimeout(function(v){v.recorder.mediaRecorder.start(1000);},500,video); // 100ms chunks + + + setTimeout(function(v) { + v.recorder.mediaRecorder.start(1000); + }, 500, video); // 100ms chunks return; } -function updateRemoteRecordButton(UUID, recorder){ - var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="'+UUID+'"]'); - if (elements[0]){ +function updateRemoteRecordButton(UUID, recorder) { + var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { var time = parseInt(recorder) || 0; - if (time==-3){ + if (time == -3) { elements[0].classList.remove("pressed"); elements[0].disabled = true; elements[0].innerHTML = ' Not Supported'; - if (!(session.cleanOutput)){ - setTimeout(function(){alert('The remote browser does not support recording.\n\nPerhaps try local recording instead.');},0); + if (!(session.cleanOutput)) { + setTimeout(function() { + alert('The remote browser does not support recording.\n\nPerhaps try local recording instead.'); + }, 0); } - - } else if (time==-2){ + + } else if (time == -2) { elements[0].classList.add("pressed"); elements[0].innerHTML = ' stopping...'; - } else if (time==-1){ + } else if (time == -1) { elements[0].classList.remove("pressed"); elements[0].innerHTML = ' Record Remote'; } else { var minutes = Math.floor(time / 60); var seconds = time - minutes * 60; elements[0].classList.add("pressed"); - elements[0].innerHTML = ' '+ minutes+"m : "+(seconds+"").padStart(2, '0')+"s"; + elements[0].innerHTML = ' ' + minutes + "m : " + (seconds + "").padStart(2, '0') + "s"; } } } -function updateLocalRecordButton(UUID, recorder){ - var elements = document.querySelectorAll('[data-action-type="recorder-local"][data--u-u-i-d="'+UUID+'"]'); - if (elements[0]){ +function updateLocalRecordButton(UUID, recorder) { + var elements = document.querySelectorAll('[data-action-type="recorder-local"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { var time = parseInt(recorder) || 0; - + //target.innerHTML = ' ARMED'; // - if (time==-3){ + if (time == -3) { elements[0].classList.add("pressed"); - elements[0].innerHTML =' ARMED'; + elements[0].innerHTML = ' ARMED'; elements[0].style.backgroundColor = "#BF3F3F"; - } else if (time==-2){ + } else if (time == -2) { elements[0].classList.add("pressed"); elements[0].innerHTML = ' stopping...'; - elements[0].style.backgroundColor =""; - } else if (time==-1){ + elements[0].style.backgroundColor = ""; + } else if (time == -1) { elements[0].classList.remove("pressed"); elements[0].innerHTML = ' Record Local'; - elements[0].style.backgroundColor =""; + elements[0].style.backgroundColor = ""; } else { var minutes = Math.floor(time / 60); var seconds = time - minutes * 60; elements[0].classList.add("pressed"); - elements[0].innerHTML = ' '+ minutes+"m : "+(seconds+"").padStart(2, '0')+"s"; - elements[0].style.backgroundColor =""; + elements[0].innerHTML = ' ' + minutes + "m : " + (seconds + "").padStart(2, '0') + "s"; + elements[0].style.backgroundColor = ""; } } } -function recordLocalVideoToggle(ele){ - if (ele.dataset.state=="0"){ - ele.dataset.state="1"; - ele.style.backgroundColor="red"; +function recordLocalVideoToggle(ele) { + if (ele.dataset.state == "0") { + ele.dataset.state = "1"; + ele.style.backgroundColor = "red"; ele.innerHTML = ''; - if ("recording" in session.videoElement){ - + if ("recording" in session.videoElement) { + } else { recordLocalVideo("start"); } } else { - if ("recording" in session.videoElement){ + if ("recording" in session.videoElement) { recordLocalVideo("stop"); } - ele.dataset.state="0"; - ele.style.backgroundColor=""; + ele.dataset.state = "0"; + ele.style.backgroundColor = ""; ele.innerHTML = ''; } } -function setupSensorData(pollrate=30){ +function setupSensorData(pollrate = 30) { session.sensors = {}; session.sensors.data = {}; session.sensors.data.sensors = true; - - if (window.Accelerometer){ - session.sensors.data.acc={}; - session.sensors.Accelerometer = new Accelerometer({frequency: pollrate}); - session.sensors.Accelerometer.addEventListener('reading', e => { + + if (window.Accelerometer) { + session.sensors.data.acc = {}; + session.sensors.Accelerometer = new Accelerometer({ + frequency: pollrate + }); + session.sensors.Accelerometer.addEventListener('reading', e => { session.sensors.data.acc.x = session.sensors.Accelerometer.x; session.sensors.data.acc.y = session.sensors.Accelerometer.y; session.sensors.data.acc.z = session.sensors.Accelerometer.z; @@ -9367,10 +10990,12 @@ function setupSensorData(pollrate=30){ }); session.sensors.Accelerometer.start(); } - if (window.Gyroscope){ - session.sensors.data.gyro={}; - session.sensors.Gyroscope = new Gyroscope({frequency: pollrate}); - session.sensors.Gyroscope.addEventListener('reading', e => { + if (window.Gyroscope) { + session.sensors.data.gyro = {}; + session.sensors.Gyroscope = new Gyroscope({ + frequency: pollrate + }); + session.sensors.Gyroscope.addEventListener('reading', e => { session.sensors.data.gyro.x = session.sensors.Gyroscope.x; session.sensors.data.gyro.y = session.sensors.Gyroscope.y; session.sensors.data.gyro.z = session.sensors.Gyroscope.z; @@ -9378,22 +11003,26 @@ function setupSensorData(pollrate=30){ }); session.sensors.Gyroscope.start(); } - if (window.Magnetometer){ - session.sensors.data.mag={}; - session.sensors.Magnetometer = new Magnetometer({frequency: pollrate}); - session.sensors.Magnetometer.addEventListener('reading', e => { + if (window.Magnetometer) { + session.sensors.data.mag = {}; + session.sensors.Magnetometer = new Magnetometer({ + frequency: pollrate + }); + session.sensors.Magnetometer.addEventListener('reading', e => { session.sensors.data.mag.x = session.sensors.Magnetometer.x; session.sensors.data.mag.y = session.sensors.Magnetometer.y; session.sensors.data.mag.z = session.sensors.Magnetometer.z; session.sensors.data.mag.t = parseInt(Math.round(session.sensors.Magnetometer.timestamp)); - + }); session.sensors.Magnetometer.start(); } - if (window.LinearAccelerationSensor){ - session.sensors.data.lin={}; - session.sensors.LinearAccelerationSensor = new LinearAccelerationSensor({frequency: pollrate}); - session.sensors.LinearAccelerationSensor.addEventListener('reading', e => { + if (window.LinearAccelerationSensor) { + session.sensors.data.lin = {}; + session.sensors.LinearAccelerationSensor = new LinearAccelerationSensor({ + frequency: pollrate + }); + session.sensors.LinearAccelerationSensor.addEventListener('reading', e => { session.sensors.data.lin.x = session.sensors.LinearAccelerationSensor.x; session.sensors.data.lin.y = session.sensors.LinearAccelerationSensor.y; session.sensors.data.lin.z = session.sensors.LinearAccelerationSensor.z; @@ -9401,41 +11030,43 @@ function setupSensorData(pollrate=30){ }); session.sensors.LinearAccelerationSensor.start(); } - setInterval(function(){firehoseSensorData();}, parseInt(1000/pollrate) ); + setInterval(function() { + firehoseSensorData(); + }, parseInt(1000 / pollrate)); } -function firehoseSensorData(){ +function firehoseSensorData() { session.sendMessage(session.sensors.data); } -if (session.sensorData){ +if (session.sensorData) { setupSensorData(parseInt(session.sensorData)); } -function recordLocalVideo(action = null, videoKbps = 6000){ // event.currentTarget,this.parentNode.parentNode.dataset.UUID +function recordLocalVideo(action = null, videoKbps = 6000) { // event.currentTarget,this.parentNode.parentNode.dataset.UUID var audioKbps = false; var video = session.videoElement; - if ("recording" in video){ - if (action=="stop"){ - log("Stopping RECORDING!"); - video.recorder.stop(); - delete(video.recorder); - delete(video.recording); - return; - } else if (action=="start"){ - log("ALREADY RECORDING!"); - getById("recordLocalbutton").dataset.state="1"; - getById("recordLocalbutton").style.backgroundColor="red"; - getById("recordLocalbutton").innerHTML = ''; - return; - } else { - log("STOPPING RECORDING by default toggle!"); - video.recorder.stop(); - return; - } + if ("recording" in video) { + if (action == "stop") { + log("Stopping RECORDING!"); + video.recorder.stop(); + delete(video.recorder); + delete(video.recording); return; - } else if (action=="start"){ - if (safariVersion()){ + } else if (action == "start") { + log("ALREADY RECORDING!"); + getById("recordLocalbutton").dataset.state = "1"; + getById("recordLocalbutton").style.backgroundColor = "red"; + getById("recordLocalbutton").innerHTML = ''; + return; + } else { + log("STOPPING RECORDING by default toggle!"); + video.recorder.stop(); + return; + } + return; + } else if (action == "start") { + if (safariVersion()) { var msg = {}; msg.UUID = session.directorUUID; msg.recorder = -3; @@ -9443,158 +11074,411 @@ function recordLocalVideo(action = null, videoKbps = 6000){ // event.currentTar return; } video.recording = true; - getById("recordLocalbutton").dataset.state="1"; - getById("recordLocalbutton").style.backgroundColor="red"; + getById("recordLocalbutton").dataset.state = "1"; + getById("recordLocalbutton").style.backgroundColor = "red"; getById("recordLocalbutton").innerHTML = ''; - } else if (action=="stop"){ + } else if (action == "stop") { return; } else { - getById("recordLocalbutton").dataset.state="1"; - getById("recordLocalbutton").style.backgroundColor="red"; + getById("recordLocalbutton").dataset.state = "1"; + getById("recordLocalbutton").style.backgroundColor = "red"; getById("recordLocalbutton").innerHTML = ''; video.recording = true; } - + video.recorder = {}; - - if (session.recordLocal!==false){ + + if (session.recordLocal !== false) { videoKbps = session.recordLocal; } - - if (videoKbps<=0){ - audioKbps = videoKbps*(-1); + + if (videoKbps <= 0) { + audioKbps = videoKbps * (-1); videoKbps = false; - } else if (videoKbps<50){ // this just makes sure you can't set 0 on the record bitrate. - videoKbps=50; + } else if (videoKbps < 50) { // this just makes sure you can't set 0 on the record bitrate. + videoKbps = 50; } - - if (typeof video.srcObject === "undefined" || !video.srcObject) {return;} - - const { readable, writable } = new TransformStream({ - transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b))) + + if (typeof video.srcObject === "undefined" || !video.srcObject) { + return; + } + + const { + readable + , writable + } = new TransformStream({ + transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b))) }); - - var filename = Date.now(); - readable.pipeTo(streamSaver.createWriteStream( filename.toString() +'.webm')); - + + var timestamp = Date.now(); + var filename = ""; + if (session.label || session.streamID) { + filename = session.label || session.streamID; + filename = filename.replace(/[\W]+/g, "_"); + filename = filename.substring(0, 200); + } + + filename += "_" + timestamp.toString(); + readable.pipeTo(streamSaver.createWriteStream(filename.toString() + '.webm')); + var writer = writable.getWriter(); video.recorder.writer = writer; - video.recorder.stop = function(restart=false){ - if (restart){ - if (getById("recordLocalbutton").dataset.state==2){ - getById("recordLocalbutton").dataset.state="0"; - getById("recordLocalbutton").style.backgroundColor=""; + video.recorder.stop = function(restart = false) { + if (restart) { + if (getById("recordLocalbutton").dataset.state == 2) { + getById("recordLocalbutton").dataset.state = "0"; + getById("recordLocalbutton").style.backgroundColor = ""; getById("recordLocalbutton").innerHTML = ''; - restart=false; + restart = false; alert("Media Recording Stopped due to an error."); } else { getById("recordLocalbutton").innerHTML = ''; - getById("recordLocalbutton").dataset.state="2"; + getById("recordLocalbutton").dataset.state = "2"; } } else { - getById("recordLocalbutton").dataset.state="0"; - getById("recordLocalbutton").style.backgroundColor=""; + getById("recordLocalbutton").dataset.state = "0"; + getById("recordLocalbutton").style.backgroundColor = ""; getById("recordLocalbutton").innerHTML = ''; } - if (!video.recording){ + if (!video.recording) { errorlog("ALREADY STOPPED"); return; } - video.recording=false; + video.recording = false; try { - if (video.recorder.mediaRecorder.state!=="inactive"){ + if (video.recorder.mediaRecorder.state !== "inactive") { video.recorder.mediaRecorder.stop(); } - } catch(e){errorlog(e);} - + } catch (e) { + errorlog(e); + } + setTimeout(() => { writer.close(); - try{ - if (session.directorUUID){ + try { + if (session.directorUUID) { var msg = {}; msg.UUID = session.directorUUID; msg.recorder = -1; session.sendMessage(msg, msg.UUID); } - } catch(e){errorlog(e);} + } catch (e) { + errorlog(e); + } delete(video.recorder); delete(video.recording); - - if (restart){ - setTimeout(function(){recordLocalVideo("start", videoKbps);},0); + + if (restart) { + setTimeout(function() { + recordLocalVideo("start", videoKbps); + }, 0); } - + }, 500); - try{ - if (session.directorUUID){ + try { + if (session.directorUUID) { var msg = {}; msg.UUID = session.directorUUID; msg.recorder = -2; session.sendMessage(msg, msg.UUID); } - } catch(e){errorlog(e);} - - }; - - let options = {}; - - if (videoKbps){ - options.mimeType = "video/webm"; - if (videoKbps<1000){ - options.videoBitsPerSecond = parseInt(videoKbps*1024); // 100 kbps audio - } else{ - options.bitsPerSecond = parseInt(videoKbps*1024); // 100 to 132 kbps audio + } catch (e) { + errorlog(e); } - video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); + + }; + + let options = {}; + + if (videoKbps) { + options.mimeType = "video/webm"; + if (videoKbps < 1000) { + options.videoBitsPerSecond = parseInt(videoKbps * 1024); // 100 kbps audio + } else { + options.bitsPerSecond = parseInt(videoKbps * 1024); // 100 to 132 kbps audio + } + video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); } else { options.mimeType = "audio/webm"; - if (audioKbps==0){ - if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")){ - options.mimeType = "audio/webm;codecs=pcm"; + if (audioKbps == 0) { + if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) { + options.mimeType = "audio/webm;codecs=pcm"; } } else { - options.bitsPerSecond = parseInt(audioKbps*1024); + options.bitsPerSecond = parseInt(audioKbps * 1024); } var stream = new MediaStream(); - video.srcObject.getAudioTracks().forEach((track)=>{ + video.srcObject.getAudioTracks().forEach((track) => { stream.addTrack(track, video.srcObject); }); - video.recorder.mediaRecorder = new MediaRecorder(stream, options); + video.recorder.mediaRecorder = new MediaRecorder(stream, options); } log(options); function handleDataAvailable(event) { - if (event.data && event.data.size > 0) { - writer.write(event.data); - if (session.directorUUID){ - if (video.recording){ + if (event.data && event.data.size > 0) { + writer.write(event.data); + if (session.directorUUID) { + if (video.recording) { var msg = {}; msg.UUID = session.directorUUID; - msg.recorder = parseInt((Date.now() - filename)/1000) || 0; + msg.recorder = parseInt((Date.now() - timestamp) / 1000) || 0; session.sendMessage(msg, msg.UUID); } } - } - } - + } + } + video.recorder.mediaRecorder.ondataavailable = handleDataAvailable; - + video.recorder.mediaRecorder.onerror = function(event) { errorlog(event); video.recorder.stop(true); }; - - video.srcObject.ended = function(event) { + + video.srcObject.ended = function(event) { video.recorder.stop(); }; - + video.recorder.mediaRecorder.start(1000); // 100ms chunks - - if (session.directorUUID){ + + if (session.directorUUID) { var msg = {}; msg.UUID = session.directorUUID; msg.recorder = 0; session.sendMessage(msg, msg.UUID); } return; -} \ No newline at end of file +} + + +if (session.midiHotkeys) { + var script = document.createElement('script'); + script.onload = function() { + WebMidi.enable(function(err) { // hotkeys + + if (err) { + errorlog(err); + } + + WebMidi.addListener("connected", function(e) { + log(e); + }); + + WebMidi.addListener("disconnected", function(e) { + log(e); + }); + + for (var i = 0; i < WebMidi.inputs.length; i++) { + var input = WebMidi.inputs[i]; + + input.addListener('noteon', "all" + , function(e) { + log(e); + var note = e.note.name + e.note.octave; + if (note == "C4") { + toggleMute(); + } + } + ); + } + }); + }; + script.src = "./thirdparty/webmidi.js"; // dynamically load this only if its needed. Keeps loading time down. + document.head.appendChild(script); +} + +document.body.innerHTML += ''; +addEventToAll(".column", 'click', function(e, ele) { + if (ele.classList.contains("skip-animation")) { + return; + } + var bounding_box = ele.getBoundingClientRect(); + ele.style.top = bounding_box.top + "px"; + ele.style.left = (bounding_box.left - 20) + "px"; + ele.classList.add('in-animation'); + ele.classList.remove('pointer'); + if (document.getElementById("empty-container")) { + getById("empty-container").parentNode.removeChid(getById("empty-container")); + } + var empty = document.createElement("DIV"); + empty.id = "empty-container"; + empty.className = "column"; + ele.parentNode.insertBefore(empty, ele.nextSibling); + const styles = "\ + @keyframes outlightbox {\ + 0% {\ + height: 100%;\ + width: 100%;\ + top: 0px;\ + left: 0px;\ + }\ + 50% {\ + height: 200px;\ + top: " + bounding_box.y + "px;\ + }\ + 100% {\ + height: 200px;\ + width: " + bounding_box.width + "px;\ + top: " + bounding_box.y + "px;\ + left: " + bounding_box.x + "px;\ + }\ + }\ + "; + if (document.getElementById('lightbox-animations')) { + getById("lightbox-animations").innerHTML = styles; + } + document.body.style.overflow = "hidden"; +}); +addEventToAll(".close", 'click', function(e, ele) { + cleanupMediaTracks(); + ele.style.display = "none"; + mapToAll(".container-inner", function(target) { + target.style.display = "none"; + }); + document.body.style.overflow = "auto"; + var bounding_box = getById("empty-container").parentNode.getBoundingClientRect(); + setTimeout(function() { // just smoothes things out; breathing room to clean up things first. + ele.parentNode.classList.add('out-animation'); + }, 1); + ele.parentNode.style.top = bounding_box.top + 'px'; + ele.parentNode.style.left = bounding_box.left + 'px'; + e.stopPropagation(); +}); +addEventToAll(".column", 'animationend', function(e, ele) { + if (e.animationName == 'inlightbox') { + ele.classList.add("skip-animation"); + mapToAll(".close", function(target) { + target.style.display = "block"; + }, ele); + mapToAll(".container-inner", function(target) { + target.style.display = "block"; + }, ele); + } else if (e.animationName == 'outlightbox') { + ele.classList.remove('in-animation'); + ele.classList.remove('out-animation'); + ele.classList.remove("skip-animation"); + ele.classList.remove('columnfade'); + ele.classList.add('pointer'); + getById("empty-container").parentNode.removeChild(getById("empty-container")); + getById("lightbox-animations").sheet.deleteRule(0); + } +}); +addEventToAll("#audioSource", 'mousedown touchend focusin focusout', function(e, ele) { + var state = getById('multiselect-trigger').dataset.state || 0; + if (state == 0) { + getById('multiselect-trigger').dataset.state = 1; + getById('multiselect-trigger').classList.add('open'); + getById('multiselect-trigger').classList.remove('closed'); + mapToAll('.chevron', function(ele) { + ele.classList.remove('bottom'); + }, parentElement = getById('multiselect-trigger')); + mapToAll('.multiselect-contents', function(ele) { + ele.style.display = "block"; + mapToAll('input[type="checkbox"]', function(ele2) { + ele2.parentNode.style.display = "block"; + ele2.style.display = "inline-block"; + }, ele) + }, parentElement = getById('multiselect-trigger').parentNode); + } + e.stopPropagation(); + e.preventDefault(); +}); +addEventToAll("#audioSource3", 'mousedown touchend focusin focusout', function(e, ele) { + var state = getById('multiselect-trigger3').dataset.state || 0; + if (state == 0) { + getById('multiselect-trigger3').dataset.state = 1; + getById('multiselect-trigger3').classList.add('open'); + getById('multiselect-trigger3').classList.remove('closed'); + mapToAll(".chevron", function(target) { + target.classList.remove('bottom'); + }, getById('multiselect-trigger3')); + mapToAll(".multiselect-contents", function(target) { + target.style.display = "block"; + }, getById('multiselect-trigger3').parentNode); + mapToAll(".multiselect-contents", function(target) { + mapToAll('input[type="checkbox"]', function(target2) { + target2.style.display = "inline-block"; + target2.parentNode.style.display = "block"; + }, target); + }, getById('multiselect-trigger3').parentNode); + } + e.stopPropagation(); + e.preventDefault(); +}); +addEventToAll("#multiselect-trigger", 'mousedown touchend focusin focusout', function(e, ele) { + var state = ele.dataset.state || 0; + if (state == 0) { // open the dropdown + ele.dataset.state = 1; + ele.classList.add('open'); + ele.classList.remove('closed'); + mapToAll(".chevron", function(target) { + target.classList.remove('bottom'); + }, getById('multiselect-trigger')); + mapToAll(".multiselect-contents", function(target) { + target.style.display = "block"; + }, ele.parentNode); + mapToAll(".multiselect-contents", function(target) { + mapToAll('input[type="checkbox"]', function(target2) { + target2.style.display = "inline-block"; + target2.parentNode.style.display = "block"; + }, target); + }, ele.parentNode); + } else { // close the dropdown + ele.dataset.state = 0; + ele.classList.add('closed'); + ele.classList.remove('open'); + mapToAll(".chevron", function(target) { + target.classList.add('bottom'); + }, ele); + mapToAll(".multiselect-contents", function(target) { + mapToAll('input[type="checkbox"]', function(target2) { + target2.style.display = "none"; + if (!target2.checked) { + target2.parentNode.style.display = "none"; + } + }, target); + }, ele.parentNode); + } + e.preventDefault(); + e.stopPropagation(); +}); +addEventToAll("#multiselect-trigger3", 'mousedown touchend focusin focusout', function(e, ele) { + var state = ele.dataset.state || 0; + if (state == 0) { // open the dropdown + ele.dataset.state = 1; + ele.classList.add('open'); + ele.classList.remove('closed'); + mapToAll(".chevron", function(target) { + target.classList.remove('bottom'); + }, ele); + mapToAll(".multiselect-contents", function(target) { + target.style.display = "block"; + }, ele.parentNode); + mapToAll(".multiselect-contents", function(target) { + mapToAll('input[type="checkbox"]', function(target2) { + target2.style.display = "inline-block"; + target2.parentNode.style.display = "block"; + }, target); + }, ele.parentNode); + } else { // close the dropdown + ele.dataset.state = 0; + ele.classList.add('closed'); + ele.classList.remove('open'); + mapToAll(".chevron", function(target) { + target.classList.add('bottom'); + }, ele); + mapToAll(".multiselect-contents", function(target) { + mapToAll('input[type="checkbox"]', function(target2) { + target2.style.display = "none"; + if (!target2.checked) { + target2.parentNode.style.display = "none"; + } + }, target); + }, ele.parentNode); + } + e.preventDefault(); + e.stopPropagation(); +}); + + diff --git a/media/cap.webm b/media/cap.webm new file mode 100644 index 0000000000000000000000000000000000000000..e8c0f078a74848c6e2b6d4fe4ca4a6945f06e1e3 GIT binary patch literal 202578 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