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 0000000..e8c0f07 Binary files /dev/null and b/media/cap.webm differ diff --git a/media/favicon-16x16.png b/media/favicon-16x16.png new file mode 100644 index 0000000..f27e085 Binary files /dev/null and b/media/favicon-16x16.png differ diff --git a/media/favicon-32x32.png b/media/favicon-32x32.png new file mode 100644 index 0000000..032c7bc Binary files /dev/null and b/media/favicon-32x32.png differ diff --git a/media/favicon.ico b/media/favicon.ico new file mode 100644 index 0000000..d022a55 Binary files /dev/null and b/media/favicon.ico differ diff --git a/media/hd.svg b/media/hd.svg new file mode 100644 index 0000000..c107866 --- /dev/null +++ b/media/hd.svg @@ -0,0 +1,2 @@ + background Layer 1 + HQ diff --git a/media/icon.png b/media/icon.png new file mode 100644 index 0000000..cb1196e Binary files /dev/null and b/media/icon.png differ diff --git a/media/obsNinja_logo_full.png b/media/obsNinja_logo_full.png new file mode 100644 index 0000000..09f509c Binary files /dev/null and b/media/obsNinja_logo_full.png differ diff --git a/media/old_logo.png b/media/old_logo.png new file mode 100644 index 0000000..aa6e1f1 Binary files /dev/null and b/media/old_logo.png differ diff --git a/media/robot.mp3 b/media/robot.mp3 new file mode 100644 index 0000000..e22c070 Binary files /dev/null and b/media/robot.mp3 differ diff --git a/media/screenshare.webm b/media/screenshare.webm new file mode 100644 index 0000000..83fa376 Binary files /dev/null and b/media/screenshare.webm differ diff --git a/media/sd.svg b/media/sd.svg new file mode 100644 index 0000000..7c3fa3e --- /dev/null +++ b/media/sd.svg @@ -0,0 +1,2 @@ + background Layer 1 + LQ diff --git a/media/share.jpg b/media/share.jpg new file mode 100644 index 0000000..50edcdf Binary files /dev/null and b/media/share.jpg differ diff --git a/media/tone.mp3 b/media/tone.mp3 new file mode 100644 index 0000000..b7c35cd Binary files /dev/null and b/media/tone.mp3 differ diff --git a/media/tone.ogg b/media/tone.ogg new file mode 100644 index 0000000..40c7e68 Binary files /dev/null and b/media/tone.ogg differ diff --git a/thirdparty/adapter.min.js b/thirdparty/adapter.min.js new file mode 100644 index 0000000..b0c7bd0 --- /dev/null +++ b/thirdparty/adapter.min.js @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + * + * FROM: https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/7.4.0/adapter.js + */ +!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).adapter=e()}(function(){return function o(a,s,c){function d(t,e){if(!s[t]){if(!a[t]){var r="function"==typeof require&&require;if(!e&&r)return r(t,!0);if(p)return p(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var i=s[t]={exports:{}};a[t][0].call(i.exports,function(e){return d(a[t][1][e]||e)},i,i.exports,o,a,s,c)}return s[t].exports}for(var p="function"==typeof require&&require,e=0;en.sctp.maxMessageSize)throw new TypeError("Message too large (can send a maximum of "+n.sctp.maxMessageSize+" bytes)");return i.apply(r,arguments)}}var r=e.RTCPeerConnection.prototype.createDataChannel;e.RTCPeerConnection.prototype.createDataChannel=function(){var e=r.apply(this,arguments);return t(e,this),e},a.wrapPeerConnectionEvent(e,"datachannel",function(e){return t(e.channel,e.target),e})},r.shimConnectionState=function(e){if(!e.RTCPeerConnection||"connectionState"in e.RTCPeerConnection.prototype)return;var r=e.RTCPeerConnection.prototype;Object.defineProperty(r,"connectionState",{get:function(){return{completed:"connected",checking:"connecting"}[this.iceConnectionState]||this.iceConnectionState},enumerable:!0,configurable:!0}),Object.defineProperty(r,"onconnectionstatechange",{get:function(){return this._onconnectionstatechange||null},set:function(e){this._onconnectionstatechange&&(this.removeEventListener("connectionstatechange",this._onconnectionstatechange),delete this._onconnectionstatechange),e&&this.addEventListener("connectionstatechange",this._onconnectionstatechange=e)},enumerable:!0,configurable:!0}),["setLocalDescription","setRemoteDescription"].forEach(function(e){var t=r[e];r[e]=function(){return this._connectionstatechangepoly||(this._connectionstatechangepoly=function(e){var t,r=e.target;return r._lastConnectionState!==r.connectionState&&(r._lastConnectionState=r.connectionState,t=new Event("connectionstatechange",e),r.dispatchEvent(t)),e},this.addEventListener("iceconnectionstatechange",this._connectionstatechangepoly)),t.apply(this,arguments)}})},r.removeAllowExtmapMixed=function(e){if(!e.RTCPeerConnection)return;var t=a.detectBrowser(e);if("chrome"===t.browser&&71<=t.version)return;var r=e.RTCPeerConnection.prototype.setRemoteDescription;e.RTCPeerConnection.prototype.setRemoteDescription=function(e){return e&&e.sdp&&-1!==e.sdp.indexOf("\na=extmap-allow-mixed")&&(e.sdp=e.sdp.split("\n").filter(function(e){return"a=extmap-allow-mixed"!==e.trim()}).join("\n")),r.apply(this,arguments)}};var n,i=e("sdp"),p=(n=i)&&n.__esModule?n:{default:n},a=function(e){{if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}}(e("./utils"))},{"./utils":11,sdp:13}],7:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.shimGetDisplayMedia=r.shimGetUserMedia=void 0;var c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n=e("./getusermedia");Object.defineProperty(r,"shimGetUserMedia",{enumerable:!0,get:function(){return n.shimGetUserMedia}});var i=e("./getdisplaymedia");Object.defineProperty(r,"shimGetDisplayMedia",{enumerable:!0,get:function(){return i.shimGetDisplayMedia}}),r.shimOnTrack=function(e){"object"===(void 0===e?"undefined":c(e))&&e.RTCTrackEvent&&"receiver"in e.RTCTrackEvent.prototype&&!("transceiver"in e.RTCTrackEvent.prototype)&&Object.defineProperty(e.RTCTrackEvent.prototype,"transceiver",{get:function(){return{receiver:this.receiver}}})},r.shimPeerConnection=function(n){var i=s.detectBrowser(n);if("object"!==(void 0===n?"undefined":c(n))||!n.RTCPeerConnection&&!n.mozRTCPeerConnection)return;!n.RTCPeerConnection&&n.mozRTCPeerConnection&&(n.RTCPeerConnection=n.mozRTCPeerConnection);i.version<53&&["setLocalDescription","setRemoteDescription","addIceCandidate"].forEach(function(e){var t=n.RTCPeerConnection.prototype[e],r=function(e,t,r){t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r;return e}({},e,function(){return arguments[0]=new("addIceCandidate"===e?n.RTCIceCandidate:n.RTCSessionDescription)(arguments[0]),t.apply(this,arguments)});n.RTCPeerConnection.prototype[e]=r[e]});{var r;i.version<68&&(r=n.RTCPeerConnection.prototype.addIceCandidate,n.RTCPeerConnection.prototype.addIceCandidate=function(e,t){return e?e&&""===e.candidate?Promise.resolve():r.apply(this,arguments):(t&&t.apply(null),Promise.resolve())})}var o={inboundrtp:"inbound-rtp",outboundrtp:"outbound-rtp",candidatepair:"candidate-pair",localcandidate:"local-candidate",remotecandidate:"remote-candidate"},a=n.RTCPeerConnection.prototype.getStats;n.RTCPeerConnection.prototype.getStats=function(){var e=Array.prototype.slice.call(arguments),t=e[0],n=e[1],r=e[2];return a.apply(this,[t||null]).then(function(r){if(i.version<53&&!n)try{r.forEach(function(e){e.type=o[e.type]||e.type})}catch(e){if("TypeError"!==e.name)throw e;r.forEach(function(e,t){r.set(t,Object.assign({},e,{type:o[e.type]||e.type}))})}return r}).then(n,r)}},r.shimSenderGetStats=function(e){if("object"!==(void 0===e?"undefined":c(e))||!e.RTCPeerConnection||!e.RTCRtpSender)return;if(e.RTCRtpSender&&"getStats"in e.RTCRtpSender.prototype)return;var r=e.RTCPeerConnection.prototype.getSenders;r&&(e.RTCPeerConnection.prototype.getSenders=function(){var t=this,e=r.apply(this,[]);return e.forEach(function(e){return e._pc=t}),e});var t=e.RTCPeerConnection.prototype.addTrack;t&&(e.RTCPeerConnection.prototype.addTrack=function(){var e=t.apply(this,arguments);return e._pc=this,e});e.RTCRtpSender.prototype.getStats=function(){return this.track?this._pc.getStats(this.track):Promise.resolve(new Map)}},r.shimReceiverGetStats=function(e){if("object"!==(void 0===e?"undefined":c(e))||!e.RTCPeerConnection||!e.RTCRtpSender)return;if(e.RTCRtpSender&&"getStats"in e.RTCRtpReceiver.prototype)return;var r=e.RTCPeerConnection.prototype.getReceivers;r&&(e.RTCPeerConnection.prototype.getReceivers=function(){var t=this,e=r.apply(this,[]);return e.forEach(function(e){return e._pc=t}),e});s.wrapPeerConnectionEvent(e,"track",function(e){return e.receiver._pc=e.srcElement,e}),e.RTCRtpReceiver.prototype.getStats=function(){return this._pc.getStats(this.track)}},r.shimRemoveStream=function(e){if(!e.RTCPeerConnection||"removeStream"in e.RTCPeerConnection.prototype)return;e.RTCPeerConnection.prototype.removeStream=function(t){var r=this;s.deprecated("removeStream","removeTrack"),this.getSenders().forEach(function(e){e.track&&t.getTracks().includes(e.track)&&r.removeTrack(e)})}},r.shimRTCDataChannel=function(e){e.DataChannel&&!e.RTCDataChannel&&(e.RTCDataChannel=e.DataChannel)},r.shimAddTransceiver=function(e){if("object"!==(void 0===e?"undefined":c(e))||!e.RTCPeerConnection)return;var s=e.RTCPeerConnection.prototype.addTransceiver;s&&(e.RTCPeerConnection.prototype.addTransceiver=function(e,t){this.setParametersPromises=[];var r=t,n=r&&"sendEncodings"in r;n&&r.sendEncodings.forEach(function(e){if("rid"in e){if(!/^[a-z0-9]{0,16}$/i.test(e.rid))throw new TypeError("Invalid RID value provided.")}if("scaleResolutionDownBy"in e&&!(1<=parseFloat(e.scaleResolutionDownBy)))throw new RangeError("scale_resolution_down_by must be >= 1.0");if("maxFramerate"in e&&!(0<=parseFloat(e.maxFramerate)))throw new RangeError("max_framerate must be >= 0.0")});var i,o,a=s.apply(this,arguments);return n&&("encodings"in(o=(i=a.sender).getParameters())||(o.encodings=r.sendEncodings,this.setParametersPromises.push(i.setParameters(o).catch(function(){})))),a})},r.shimCreateOffer=function(e){if("object"!==(void 0===e?"undefined":c(e))||!e.RTCPeerConnection)return;var r=e.RTCPeerConnection.prototype.createOffer;e.RTCPeerConnection.prototype.createOffer=function(){var e=this,t=arguments;return this.setParametersPromises&&this.setParametersPromises.length?Promise.all(this.setParametersPromises).then(function(){return r.apply(e,t)}).finally(function(){e.setParametersPromises=[]}):r.apply(this,arguments)}},r.shimCreateAnswer=function(e){if("object"!==(void 0===e?"undefined":c(e))||!e.RTCPeerConnection)return;var r=e.RTCPeerConnection.prototype.createAnswer;e.RTCPeerConnection.prototype.createAnswer=function(){var e=this,t=arguments;return this.setParametersPromises&&this.setParametersPromises.length?Promise.all(this.setParametersPromises).then(function(){return r.apply(e,t)}).finally(function(){e.setParametersPromises=[]}):r.apply(this,arguments)}};var s=function(e){{if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}}(e("../utils"))},{"../utils":11,"./getdisplaymedia":8,"./getusermedia":9}],8:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.shimGetDisplayMedia=function(r,n){if(r.navigator.mediaDevices&&"getDisplayMedia"in r.navigator.mediaDevices)return;if(!r.navigator.mediaDevices)return;r.navigator.mediaDevices.getDisplayMedia=function(e){if(e&&e.video)return!0===e.video?e.video={mediaSource:n}:e.video.mediaSource=n,r.navigator.mediaDevices.getUserMedia(e);var t=new DOMException("getDisplayMedia without video constraints is undefined");return t.name="NotFoundError",t.code=8,Promise.reject(t)}}},{}],9:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};r.shimGetUserMedia=function(e){var t=d.detectBrowser(e),n=e&&e.navigator,r=e&&e.MediaStreamTrack;{var i,o,a,s;n.getUserMedia=function(e,t,r){d.deprecated("navigator.getUserMedia","navigator.mediaDevices.getUserMedia"),n.mediaDevices.getUserMedia(e).then(t,r)},55=r&&parseInt(n[r],10)}function c(e){return"[object Object]"===Object.prototype.toString.call(e)}function d(t,r,n){r&&!n.has(r.id)&&(n.set(r.id,r),Object.keys(r).forEach(function(e){e.endsWith("Id")?d(t,t.get(r[e]),n):e.endsWith("Ids")&&r[e].forEach(function(e){d(t,t.get(e),n)})}))}},{}],12:[function(e,t,r){},{}],13:[function(e,t,r){"use strict";var p={generateIdentifier:function(){return Math.random().toString(36).substr(2,10)}};p.localCName=p.generateIdentifier(),p.splitLines=function(e){return e.trim().split("\n").map(function(e){return e.trim()})},p.splitSections=function(e){return e.split("\nm=").map(function(e,t){return(0n&&(n=e.maxptime)}),0\n\nconst SymbolPolyfill: (description?: string) => symbol =\n typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' ?\n Symbol :\n description => `Symbol(${description})` as any as symbol;\n\nexport default SymbolPolyfill;\n","/// \n\nexport function noop() {\n // do nothing\n}\n\nfunction getGlobals() {\n if (typeof self !== 'undefined') {\n return self;\n } else if (typeof window !== 'undefined') {\n return window;\n } else if (typeof global !== 'undefined') {\n return global;\n }\n return undefined;\n}\n\nexport const globals = getGlobals();\n","import { noop } from '../../utils';\nimport { AssertionError } from '../../stub/assert';\n\nexport function typeIsObject(x: any): x is object {\n return (typeof x === 'object' && x !== null) || typeof x === 'function';\n}\n\nexport const rethrowAssertionErrorRejection: (e: any) => void =\n DEBUG ? e => {\n // Used throughout the reference implementation, as `.catch(rethrowAssertionErrorRejection)`, to ensure any errors\n // get shown. There are places in the spec where we do promise transformations and purposefully ignore or don't\n // expect any errors, but assertion errors are always problematic.\n if (e && e instanceof AssertionError) {\n setTimeout(() => {\n throw e;\n }, 0);\n }\n } : noop;\n","import { globals } from '../../utils';\nimport { rethrowAssertionErrorRejection } from './miscellaneous';\nimport assert from '../../stub/assert';\n\nconst originalPromise = Promise;\nconst originalPromiseThen = Promise.prototype.then;\nconst originalPromiseResolve = Promise.resolve.bind(originalPromise);\nconst originalPromiseReject = Promise.reject.bind(originalPromise);\n\nexport function newPromise(executor: (\n resolve: (value?: T | PromiseLike) => void,\n reject: (reason?: any) => void\n) => void): Promise {\n return new originalPromise(executor);\n}\n\nexport function promiseResolvedWith(value: T | PromiseLike): Promise {\n return originalPromiseResolve(value);\n}\n\nexport function promiseRejectedWith(reason: any): Promise {\n return originalPromiseReject(reason);\n}\n\nexport function PerformPromiseThen(\n promise: Promise,\n onFulfilled?: (value: T) => TResult1 | PromiseLike,\n onRejected?: (reason: any) => TResult2 | PromiseLike): Promise {\n // There doesn't appear to be any way to correctly emulate the behaviour from JavaScript, so this is just an\n // approximation.\n return originalPromiseThen.call(promise, onFulfilled, onRejected) as Promise;\n}\n\nexport function uponPromise(\n promise: Promise,\n onFulfilled?: (value: T) => void | PromiseLike,\n onRejected?: (reason: any) => void | PromiseLike): void {\n PerformPromiseThen(\n PerformPromiseThen(promise, onFulfilled, onRejected),\n undefined,\n rethrowAssertionErrorRejection\n );\n}\n\nexport function uponFulfillment(promise: Promise, onFulfilled: (value: T) => void | PromiseLike): void {\n uponPromise(promise, onFulfilled);\n}\n\nexport function uponRejection(promise: Promise, onRejected: (reason: any) => void | PromiseLike): void {\n uponPromise(promise, undefined, onRejected);\n}\n\nexport function transformPromiseWith(\n promise: Promise,\n fulfillmentHandler?: (value: T) => TResult1 | PromiseLike,\n rejectionHandler?: (reason: any) => TResult2 | PromiseLike): Promise {\n return PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler);\n}\n\nexport function setPromiseIsHandledToTrue(promise: Promise): void {\n PerformPromiseThen(promise, undefined, rethrowAssertionErrorRejection);\n}\n\nexport const queueMicrotask: (fn: () => void) => void = (() => {\n const globalQueueMicrotask = globals && globals.queueMicrotask;\n if (typeof globalQueueMicrotask === 'function') {\n return globalQueueMicrotask;\n }\n\n const resolvedPromise = promiseResolvedWith(undefined);\n return (fn: () => void) => PerformPromiseThen(resolvedPromise, fn);\n})();\n\nexport function reflectCall(F: (this: T, ...args: A) => R, V: T, args: A): R {\n if (typeof F !== 'function') {\n throw new TypeError('Argument is not a function');\n }\n return Function.prototype.apply.call(F, V, args);\n}\n\nexport function promiseCall(F: (this: T, ...args: A) => R | PromiseLike,\n V: T,\n args: A): Promise {\n assert(typeof F === 'function');\n assert(V !== undefined);\n assert(Array.isArray(args));\n try {\n return promiseResolvedWith(reflectCall(F, V, args));\n } catch (value) {\n return promiseRejectedWith(value);\n }\n}\n","import assert from '../stub/assert';\n\n// Original from Chromium\n// https://chromium.googlesource.com/chromium/src/+/0aee4434a4dba42a42abaea9bfbc0cd196a63bc1/third_party/blink/renderer/core/streams/SimpleQueue.js\n\nconst QUEUE_MAX_ARRAY_SIZE = 16384;\n\ninterface Node {\n _elements: T[];\n _next: Node | undefined;\n}\n\n/**\n * Simple queue structure.\n *\n * Avoids scalability issues with using a packed array directly by using\n * multiple arrays in a linked list and keeping the array size bounded.\n */\nexport class SimpleQueue {\n private _front: Node;\n private _back: Node;\n private _cursor = 0;\n private _size = 0;\n\n constructor() {\n // _front and _back are always defined.\n this._front = {\n _elements: [],\n _next: undefined\n };\n this._back = this._front;\n // The cursor is used to avoid calling Array.shift().\n // It contains the index of the front element of the array inside the\n // front-most node. It is always in the range [0, QUEUE_MAX_ARRAY_SIZE).\n this._cursor = 0;\n // When there is only one node, size === elements.length - cursor.\n this._size = 0;\n }\n\n get length(): number {\n return this._size;\n }\n\n // For exception safety, this method is structured in order:\n // 1. Read state\n // 2. Calculate required state mutations\n // 3. Perform state mutations\n push(element: T): void {\n const oldBack = this._back;\n let newBack = oldBack;\n assert(oldBack._next === undefined);\n if (oldBack._elements.length === QUEUE_MAX_ARRAY_SIZE - 1) {\n newBack = {\n _elements: [],\n _next: undefined\n };\n }\n\n // push() is the mutation most likely to throw an exception, so it\n // goes first.\n oldBack._elements.push(element);\n if (newBack !== oldBack) {\n this._back = newBack;\n oldBack._next = newBack;\n }\n ++this._size;\n }\n\n // Like push(), shift() follows the read -> calculate -> mutate pattern for\n // exception safety.\n shift(): T {\n assert(this._size > 0); // must not be called on an empty queue\n\n const oldFront = this._front;\n let newFront = oldFront;\n const oldCursor = this._cursor;\n let newCursor = oldCursor + 1;\n\n const elements = oldFront._elements;\n const element = elements[oldCursor];\n\n if (newCursor === QUEUE_MAX_ARRAY_SIZE) {\n assert(elements.length === QUEUE_MAX_ARRAY_SIZE);\n assert(oldFront._next !== undefined);\n newFront = oldFront._next!;\n newCursor = 0;\n }\n\n // No mutations before this point.\n --this._size;\n this._cursor = newCursor;\n if (oldFront !== newFront) {\n this._front = newFront;\n }\n\n // Permit shifted element to be garbage collected.\n elements[oldCursor] = undefined!;\n\n return element;\n }\n\n // The tricky thing about forEach() is that it can be called\n // re-entrantly. The queue may be mutated inside the callback. It is easy to\n // see that push() within the callback has no negative effects since the end\n // of the queue is checked for on every iteration. If shift() is called\n // repeatedly within the callback then the next iteration may return an\n // element that has been removed. In this case the callback will be called\n // with undefined values until we either \"catch up\" with elements that still\n // exist or reach the back of the queue.\n forEach(callback: (element: T) => void): void {\n let i = this._cursor;\n let node = this._front;\n let elements = node._elements;\n while (i !== elements.length || node._next !== undefined) {\n if (i === elements.length) {\n assert(node._next !== undefined);\n assert(i === QUEUE_MAX_ARRAY_SIZE);\n node = node._next!;\n elements = node._elements;\n i = 0;\n if (elements.length === 0) {\n break;\n }\n }\n callback(elements[i]);\n ++i;\n }\n }\n\n // Return the element that would be returned if shift() was called now,\n // without modifying the queue.\n peek() {\n assert(this._size > 0); // must not be called on an empty queue\n\n const front = this._front;\n const cursor = this._cursor;\n return front._elements[cursor];\n }\n}\n","import assert from '../../stub/assert';\nimport { ReadableStream, ReadableStreamCancel, ReadableStreamReader } from '../readable-stream';\nimport { newPromise, setPromiseIsHandledToTrue } from '../helpers/webidl';\n\nexport function ReadableStreamReaderGenericInitialize(reader: ReadableStreamReader, stream: ReadableStream) {\n reader._ownerReadableStream = stream;\n stream._reader = reader;\n\n if (stream._state === 'readable') {\n defaultReaderClosedPromiseInitialize(reader);\n } else if (stream._state === 'closed') {\n defaultReaderClosedPromiseInitializeAsResolved(reader);\n } else {\n assert(stream._state === 'errored');\n\n defaultReaderClosedPromiseInitializeAsRejected(reader, stream._storedError);\n }\n}\n\n// A client of ReadableStreamDefaultReader and ReadableStreamBYOBReader may use these functions directly to bypass state\n// check.\n\nexport function ReadableStreamReaderGenericCancel(reader: ReadableStreamReader, reason: any): Promise {\n const stream = reader._ownerReadableStream;\n assert(stream !== undefined);\n return ReadableStreamCancel(stream, reason);\n}\n\nexport function ReadableStreamReaderGenericRelease(reader: ReadableStreamReader) {\n assert(reader._ownerReadableStream !== undefined);\n assert(reader._ownerReadableStream._reader === reader);\n\n if (reader._ownerReadableStream._state === 'readable') {\n defaultReaderClosedPromiseReject(\n reader,\n new TypeError(`Reader was released and can no longer be used to monitor the stream's closedness`));\n } else {\n defaultReaderClosedPromiseResetToRejected(\n reader,\n new TypeError(`Reader was released and can no longer be used to monitor the stream's closedness`));\n }\n\n reader._ownerReadableStream._reader = undefined;\n reader._ownerReadableStream = undefined!;\n}\n\n// Helper functions for the readers.\n\nexport function readerLockException(name: string): TypeError {\n return new TypeError('Cannot ' + name + ' a stream using a released reader');\n}\n\n// Helper functions for the ReadableStreamDefaultReader.\n\nexport function defaultReaderClosedPromiseInitialize(reader: ReadableStreamReader) {\n reader._closedPromise = newPromise((resolve, reject) => {\n reader._closedPromise_resolve = resolve;\n reader._closedPromise_reject = reject;\n });\n}\n\nexport function defaultReaderClosedPromiseInitializeAsRejected(reader: ReadableStreamReader, reason: any) {\n defaultReaderClosedPromiseInitialize(reader);\n defaultReaderClosedPromiseReject(reader, reason);\n}\n\nexport function defaultReaderClosedPromiseInitializeAsResolved(reader: ReadableStreamReader) {\n defaultReaderClosedPromiseInitialize(reader);\n defaultReaderClosedPromiseResolve(reader);\n}\n\nexport function defaultReaderClosedPromiseReject(reader: ReadableStreamReader, reason: any) {\n if (reader._closedPromise_reject === undefined) {\n return;\n }\n\n setPromiseIsHandledToTrue(reader._closedPromise);\n reader._closedPromise_reject(reason);\n reader._closedPromise_resolve = undefined;\n reader._closedPromise_reject = undefined;\n}\n\nexport function defaultReaderClosedPromiseResetToRejected(reader: ReadableStreamReader, reason: any) {\n assert(reader._closedPromise_resolve === undefined);\n assert(reader._closedPromise_reject === undefined);\n\n defaultReaderClosedPromiseInitializeAsRejected(reader, reason);\n}\n\nexport function defaultReaderClosedPromiseResolve(reader: ReadableStreamReader) {\n if (reader._closedPromise_resolve === undefined) {\n return;\n }\n\n reader._closedPromise_resolve(undefined);\n reader._closedPromise_resolve = undefined;\n reader._closedPromise_reject = undefined;\n}\n","export const AbortSteps = Symbol('[[AbortSteps]]');\nexport const ErrorSteps = Symbol('[[ErrorSteps]]');\nexport const CancelSteps = Symbol('[[CancelSteps]]');\nexport const PullSteps = Symbol('[[PullSteps]]');\n","/// \n\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite#Polyfill\nconst NumberIsFinite: typeof Number.isFinite = Number.isFinite || function (x) {\n return typeof x === 'number' && isFinite(x);\n};\n\nexport default NumberIsFinite;\n","/// \n\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc#Polyfill\nconst MathTrunc: typeof Math.trunc = Math.trunc || function (v) {\n return v < 0 ? Math.ceil(v) : Math.floor(v);\n};\n\nexport default MathTrunc;\n","import NumberIsFinite from '../../stub/number-isfinite';\nimport MathTrunc from '../../stub/math-trunc';\n\n// https://heycam.github.io/webidl/#idl-dictionaries\nexport function isDictionary(x: any): x is object | null {\n return typeof x === 'object' || typeof x === 'function';\n}\n\nexport function assertDictionary(obj: unknown,\n context: string): asserts obj is object | null | undefined {\n if (obj !== undefined && !isDictionary(obj)) {\n throw new TypeError(`${context} is not an object.`);\n }\n}\n\nexport type AnyFunction = (...args: any[]) => any;\n\n// https://heycam.github.io/webidl/#idl-callback-functions\nexport function assertFunction(x: unknown, context: string): asserts x is AnyFunction {\n if (typeof x !== 'function') {\n throw new TypeError(`${context} is not a function.`);\n }\n}\n\n// https://heycam.github.io/webidl/#idl-object\nexport function isObject(x: any): x is object {\n return (typeof x === 'object' && x !== null) || typeof x === 'function';\n}\n\nexport function assertObject(x: unknown,\n context: string): asserts x is object {\n if (!isObject(x)) {\n throw new TypeError(`${context} is not an object.`);\n }\n}\n\nexport function assertRequiredArgument(x: T | undefined,\n position: number,\n context: string): asserts x is T {\n if (x === undefined) {\n throw new TypeError(`Parameter ${position} is required in '${context}'.`);\n }\n}\n\nexport function assertRequiredField(x: T | undefined,\n field: string,\n context: string): asserts x is T {\n if (x === undefined) {\n throw new TypeError(`${field} is required in '${context}'.`);\n }\n}\n\n// https://heycam.github.io/webidl/#idl-unrestricted-double\nexport function convertUnrestrictedDouble(value: unknown): number {\n return Number(value);\n}\n\nfunction censorNegativeZero(x: number): number {\n return x === 0 ? 0 : x;\n}\n\nfunction integerPart(x: number): number {\n return censorNegativeZero(MathTrunc(x));\n}\n\n// https://heycam.github.io/webidl/#idl-unsigned-long-long\nexport function convertUnsignedLongLongWithEnforceRange(value: unknown, context: string): number {\n const lowerBound = 0;\n const upperBound = Number.MAX_SAFE_INTEGER;\n\n let x = Number(value);\n x = censorNegativeZero(x);\n\n if (!NumberIsFinite(x)) {\n throw new TypeError(`${context} is not a finite number`);\n }\n\n x = integerPart(x);\n\n if (x < lowerBound || x > upperBound) {\n throw new TypeError(`${context} is outside the accepted range of ${lowerBound} to ${upperBound}, inclusive`);\n }\n\n if (!NumberIsFinite(x) || x === 0) {\n return 0;\n }\n\n // TODO Use BigInt if supported?\n // let xBigInt = BigInt(integerPart(x));\n // xBigInt = BigInt.asUintN(64, xBigInt);\n // return Number(xBigInt);\n\n return x;\n}\n","import { IsReadableStream, ReadableStream } from '../readable-stream';\n\nexport function assertReadableStream(x: unknown, context: string): asserts x is ReadableStream {\n if (!IsReadableStream(x)) {\n throw new TypeError(`${context} is not a ReadableStream.`);\n }\n}\n","import assert from '../../stub/assert';\nimport { SimpleQueue } from '../simple-queue';\nimport {\n ReadableStreamReaderGenericCancel,\n ReadableStreamReaderGenericInitialize,\n ReadableStreamReaderGenericRelease,\n readerLockException\n} from './generic-reader';\nimport { IsReadableStreamLocked, ReadableStream } from '../readable-stream';\nimport { typeIsObject } from '../helpers/miscellaneous';\nimport { PullSteps } from '../abstract-ops/internal-methods';\nimport { newPromise, promiseRejectedWith } from '../helpers/webidl';\nimport { assertRequiredArgument } from '../validators/basic';\nimport { assertReadableStream } from '../validators/readable-stream';\n\n/**\n * A result returned by {@link ReadableStreamDefaultReader.read}.\n *\n * @public\n */\nexport type ReadableStreamDefaultReadResult = {\n done: false;\n value: T;\n} | {\n done: true;\n value: undefined;\n}\n\n// Abstract operations for the ReadableStream.\n\nexport function AcquireReadableStreamDefaultReader(stream: ReadableStream): ReadableStreamDefaultReader {\n return new ReadableStreamDefaultReader(stream);\n}\n\n// ReadableStream API exposed for controllers.\n\nexport function ReadableStreamAddReadRequest(stream: ReadableStream,\n readRequest: ReadRequest): void {\n assert(IsReadableStreamDefaultReader(stream._reader));\n assert(stream._state === 'readable');\n\n (stream._reader! as ReadableStreamDefaultReader)._readRequests.push(readRequest);\n}\n\nexport function ReadableStreamFulfillReadRequest(stream: ReadableStream, chunk: R | undefined, done: boolean) {\n const reader = stream._reader as ReadableStreamDefaultReader;\n\n assert(reader._readRequests.length > 0);\n\n const readRequest = reader._readRequests.shift()!;\n if (done) {\n readRequest._closeSteps();\n } else {\n readRequest._chunkSteps(chunk!);\n }\n}\n\nexport function ReadableStreamGetNumReadRequests(stream: ReadableStream): number {\n return (stream._reader as ReadableStreamDefaultReader)._readRequests.length;\n}\n\nexport function ReadableStreamHasDefaultReader(stream: ReadableStream): boolean {\n const reader = stream._reader;\n\n if (reader === undefined) {\n return false;\n }\n\n if (!IsReadableStreamDefaultReader(reader)) {\n return false;\n }\n\n return true;\n}\n\n// Readers\n\nexport interface ReadRequest {\n _chunkSteps(chunk: R): void;\n\n _closeSteps(): void;\n\n _errorSteps(e: any): void;\n}\n\n/**\n * A default reader vended by a {@link ReadableStream}.\n *\n * @public\n */\nexport class ReadableStreamDefaultReader {\n /** @internal */\n _ownerReadableStream!: ReadableStream;\n /** @internal */\n _closedPromise!: Promise;\n /** @internal */\n _closedPromise_resolve?: (value?: undefined) => void;\n /** @internal */\n _closedPromise_reject?: (reason: any) => void;\n /** @internal */\n _readRequests: SimpleQueue>;\n\n constructor(stream: ReadableStream) {\n assertRequiredArgument(stream, 1, 'ReadableStreamDefaultReader');\n assertReadableStream(stream, 'First parameter');\n\n if (IsReadableStreamLocked(stream)) {\n throw new TypeError('This stream has already been locked for exclusive reading by another reader');\n }\n\n ReadableStreamReaderGenericInitialize(this, stream);\n\n this._readRequests = new SimpleQueue();\n }\n\n /**\n * Returns a promise that will be fulfilled when the stream becomes closed,\n * or rejected if the stream ever errors or the reader's lock is released before the stream finishes closing.\n */\n get closed(): Promise {\n if (!IsReadableStreamDefaultReader(this)) {\n return promiseRejectedWith(defaultReaderBrandCheckException('closed'));\n }\n\n return this._closedPromise;\n }\n\n /**\n * If the reader is active, behaves the same as {@link ReadableStream.cancel | stream.cancel(reason)}.\n */\n cancel(reason: any = undefined): Promise {\n if (!IsReadableStreamDefaultReader(this)) {\n return promiseRejectedWith(defaultReaderBrandCheckException('cancel'));\n }\n\n if (this._ownerReadableStream === undefined) {\n return promiseRejectedWith(readerLockException('cancel'));\n }\n\n return ReadableStreamReaderGenericCancel(this, reason);\n }\n\n /**\n * Returns a promise that allows access to the next chunk from the stream's internal queue, if available.\n *\n * If reading a chunk causes the queue to become empty, more data will be pulled from the underlying source.\n */\n read(): Promise> {\n if (!IsReadableStreamDefaultReader(this)) {\n return promiseRejectedWith(defaultReaderBrandCheckException('read'));\n }\n\n if (this._ownerReadableStream === undefined) {\n return promiseRejectedWith(readerLockException('read from'));\n }\n\n let resolvePromise!: (result: ReadableStreamDefaultReadResult) => void;\n let rejectPromise!: (reason: any) => void;\n const promise = newPromise>((resolve, reject) => {\n resolvePromise = resolve;\n rejectPromise = reject;\n });\n const readRequest: ReadRequest = {\n _chunkSteps: chunk => resolvePromise({ value: chunk, done: false }),\n _closeSteps: () => resolvePromise({ value: undefined, done: true }),\n _errorSteps: e => rejectPromise(e)\n };\n ReadableStreamDefaultReaderRead(this, readRequest);\n return promise;\n }\n\n /**\n * Releases the reader's lock on the corresponding stream. After the lock is released, the reader is no longer active.\n * If the associated stream is errored when the lock is released, the reader will appear errored in the same way\n * from now on; otherwise, the reader will appear closed.\n *\n * A reader's lock cannot be released while it still has a pending read request, i.e., if a promise returned by\n * the reader's {@link ReadableStreamDefaultReader.read | read()} method has not yet been settled. Attempting to\n * do so will throw a `TypeError` and leave the reader locked to the stream.\n */\n releaseLock(): void {\n if (!IsReadableStreamDefaultReader(this)) {\n throw defaultReaderBrandCheckException('releaseLock');\n }\n\n if (this._ownerReadableStream === undefined) {\n return;\n }\n\n if (this._readRequests.length > 0) {\n throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled');\n }\n\n ReadableStreamReaderGenericRelease(this);\n }\n}\n\nObject.defineProperties(ReadableStreamDefaultReader.prototype, {\n cancel: { enumerable: true },\n read: { enumerable: true },\n releaseLock: { enumerable: true },\n closed: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(ReadableStreamDefaultReader.prototype, Symbol.toStringTag, {\n value: 'ReadableStreamDefaultReader',\n configurable: true\n });\n}\n\n// Abstract operations for the readers.\n\nexport function IsReadableStreamDefaultReader(x: any): x is ReadableStreamDefaultReader {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_readRequests')) {\n return false;\n }\n\n return true;\n}\n\nexport function ReadableStreamDefaultReaderRead(reader: ReadableStreamDefaultReader,\n readRequest: ReadRequest): void {\n const stream = reader._ownerReadableStream;\n\n assert(stream !== undefined);\n\n stream._disturbed = true;\n\n if (stream._state === 'closed') {\n readRequest._closeSteps();\n } else if (stream._state === 'errored') {\n readRequest._errorSteps(stream._storedError);\n } else {\n assert(stream._state === 'readable');\n stream._readableStreamController[PullSteps](readRequest as ReadRequest);\n }\n}\n\n// Helper functions for the ReadableStreamDefaultReader.\n\nfunction defaultReaderBrandCheckException(name: string): TypeError {\n return new TypeError(\n `ReadableStreamDefaultReader.prototype.${name} can only be used on a ReadableStreamDefaultReader`);\n}\n","/// \n\nexport let AsyncIteratorPrototype: AsyncIterable | undefined;\n\nif (typeof Symbol.asyncIterator === 'symbol') {\n // We're running inside a ES2018+ environment, but we're compiling to an older syntax.\n // We cannot access %AsyncIteratorPrototype% without non-ES2018 syntax, but we can re-create it.\n AsyncIteratorPrototype = {\n // 25.1.3.1 %AsyncIteratorPrototype% [ @@asyncIterator ] ( )\n // https://tc39.github.io/ecma262/#sec-asynciteratorprototype-asynciterator\n [Symbol.asyncIterator]() {\n return this as any;\n }\n };\n Object.defineProperty(AsyncIteratorPrototype, Symbol.asyncIterator, { enumerable: false });\n}\n","/// \n\nimport { ReadableStream } from '../readable-stream';\nimport {\n AcquireReadableStreamDefaultReader,\n ReadableStreamDefaultReader,\n ReadableStreamDefaultReaderRead,\n ReadableStreamDefaultReadResult,\n ReadRequest\n} from './default-reader';\nimport {\n ReadableStreamReaderGenericCancel,\n ReadableStreamReaderGenericRelease,\n readerLockException\n} from './generic-reader';\nimport assert from '../../stub/assert';\nimport { AsyncIteratorPrototype } from '@@target/stub/async-iterator-prototype';\nimport { typeIsObject } from '../helpers/miscellaneous';\nimport {\n newPromise,\n promiseRejectedWith,\n promiseResolvedWith,\n queueMicrotask,\n transformPromiseWith\n} from '../helpers/webidl';\n\n/**\n * An async iterator returned by {@link ReadableStream.values}.\n *\n * @public\n */\nexport interface ReadableStreamAsyncIterator extends AsyncIterator {\n next(): Promise>;\n\n return(value?: any): Promise>;\n}\n\nexport class ReadableStreamAsyncIteratorImpl {\n private readonly _reader: ReadableStreamDefaultReader;\n private readonly _preventCancel: boolean;\n private _ongoingPromise: Promise> | undefined = undefined;\n private _isFinished = false;\n\n constructor(reader: ReadableStreamDefaultReader, preventCancel: boolean) {\n this._reader = reader;\n this._preventCancel = preventCancel;\n }\n\n next(): Promise> {\n const nextSteps = () => this._nextSteps();\n this._ongoingPromise = this._ongoingPromise ?\n transformPromiseWith(this._ongoingPromise, nextSteps, nextSteps) :\n nextSteps();\n return this._ongoingPromise;\n }\n\n return(value: any): Promise> {\n const returnSteps = () => this._returnSteps(value);\n return this._ongoingPromise ?\n transformPromiseWith(this._ongoingPromise, returnSteps, returnSteps) :\n returnSteps();\n }\n\n private _nextSteps(): Promise> {\n if (this._isFinished) {\n return Promise.resolve({ value: undefined, done: true });\n }\n\n const reader = this._reader;\n if (reader._ownerReadableStream === undefined) {\n return promiseRejectedWith(readerLockException('iterate'));\n }\n\n let resolvePromise!: (result: ReadableStreamDefaultReadResult) => void;\n let rejectPromise!: (reason: any) => void;\n const promise = newPromise>((resolve, reject) => {\n resolvePromise = resolve;\n rejectPromise = reject;\n });\n const readRequest: ReadRequest = {\n _chunkSteps: chunk => {\n this._ongoingPromise = undefined;\n // This needs to be delayed by one microtask, otherwise we stop pulling too early which breaks a test.\n // FIXME Is this a bug in the specification, or in the test?\n queueMicrotask(() => resolvePromise({ value: chunk, done: false }));\n },\n _closeSteps: () => {\n this._ongoingPromise = undefined;\n this._isFinished = true;\n ReadableStreamReaderGenericRelease(reader);\n resolvePromise({ value: undefined, done: true });\n },\n _errorSteps: reason => {\n this._ongoingPromise = undefined;\n this._isFinished = true;\n ReadableStreamReaderGenericRelease(reader);\n rejectPromise(reason);\n }\n };\n ReadableStreamDefaultReaderRead(reader, readRequest);\n return promise;\n }\n\n private _returnSteps(value: any): Promise> {\n if (this._isFinished) {\n return Promise.resolve({ value, done: true });\n }\n this._isFinished = true;\n\n const reader = this._reader;\n if (reader._ownerReadableStream === undefined) {\n return promiseRejectedWith(readerLockException('finish iterating'));\n }\n\n assert(reader._readRequests.length === 0);\n\n if (!this._preventCancel) {\n const result = ReadableStreamReaderGenericCancel(reader, value);\n ReadableStreamReaderGenericRelease(reader);\n return transformPromiseWith(result, () => ({ value, done: true }));\n }\n\n ReadableStreamReaderGenericRelease(reader);\n return promiseResolvedWith({ value, done: true });\n }\n}\n\ndeclare class ReadableStreamAsyncIteratorInstance implements ReadableStreamAsyncIterator {\n /** @interal */\n _asyncIteratorImpl: ReadableStreamAsyncIteratorImpl;\n\n next(): Promise>;\n\n return(value?: any): Promise>;\n}\n\nconst ReadableStreamAsyncIteratorPrototype: ReadableStreamAsyncIteratorInstance = {\n next(this: ReadableStreamAsyncIteratorInstance): Promise> {\n if (!IsReadableStreamAsyncIterator(this)) {\n return promiseRejectedWith(streamAsyncIteratorBrandCheckException('next'));\n }\n return this._asyncIteratorImpl.next();\n },\n\n return(this: ReadableStreamAsyncIteratorInstance, value: any): Promise> {\n if (!IsReadableStreamAsyncIterator(this)) {\n return promiseRejectedWith(streamAsyncIteratorBrandCheckException('return'));\n }\n return this._asyncIteratorImpl.return(value);\n }\n} as any;\nif (AsyncIteratorPrototype !== undefined) {\n Object.setPrototypeOf(ReadableStreamAsyncIteratorPrototype, AsyncIteratorPrototype);\n}\n\n// Abstract operations for the ReadableStream.\n\nexport function AcquireReadableStreamAsyncIterator(stream: ReadableStream,\n preventCancel: boolean): ReadableStreamAsyncIterator {\n const reader = AcquireReadableStreamDefaultReader(stream);\n const impl = new ReadableStreamAsyncIteratorImpl(reader, preventCancel);\n const iterator: ReadableStreamAsyncIteratorInstance = Object.create(ReadableStreamAsyncIteratorPrototype);\n iterator._asyncIteratorImpl = impl;\n return iterator;\n}\n\nfunction IsReadableStreamAsyncIterator(x: any): x is ReadableStreamAsyncIterator {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_asyncIteratorImpl')) {\n return false;\n }\n\n return true;\n}\n\n// Helper functions for the ReadableStream.\n\nfunction streamAsyncIteratorBrandCheckException(name: string): TypeError {\n return new TypeError(`ReadableStreamAsyncIterator.${name} can only be used on a ReadableSteamAsyncIterator`);\n}\n","/// \n\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN#Polyfill\nconst NumberIsNaN: typeof Number.isNaN = Number.isNaN || function (x) {\n // eslint-disable-next-line no-self-compare\n return x !== x;\n};\n\nexport default NumberIsNaN;\n","import NumberIsNaN from '../../stub/number-isnan';\n\nexport function IsFiniteNonNegativeNumber(v: number): boolean {\n if (!IsNonNegativeNumber(v)) {\n return false;\n }\n\n if (v === Infinity) {\n return false;\n }\n\n return true;\n}\n\nexport function IsNonNegativeNumber(v: number): boolean {\n if (typeof v !== 'number') {\n return false;\n }\n\n if (NumberIsNaN(v)) {\n return false;\n }\n\n if (v < 0) {\n return false;\n }\n\n return true;\n}\n","import assert from '../../stub/assert';\nimport { SimpleQueue } from '../simple-queue';\nimport { IsFiniteNonNegativeNumber } from './miscellaneous';\n\nexport interface QueueContainer {\n _queue: SimpleQueue;\n _queueTotalSize: number;\n}\n\nexport interface QueuePair {\n value: T;\n size: number;\n}\n\nexport function DequeueValue(container: QueueContainer>): T {\n assert('_queue' in container && '_queueTotalSize' in container);\n assert(container._queue.length > 0);\n\n const pair = container._queue.shift()!;\n container._queueTotalSize -= pair.size;\n if (container._queueTotalSize < 0) {\n container._queueTotalSize = 0;\n }\n\n return pair.value;\n}\n\nexport function EnqueueValueWithSize(container: QueueContainer>, value: T, size: number) {\n assert('_queue' in container && '_queueTotalSize' in container);\n\n size = Number(size);\n if (!IsFiniteNonNegativeNumber(size)) {\n throw new RangeError('Size must be a finite, non-NaN, non-negative number.');\n }\n\n container._queue.push({ value, size });\n container._queueTotalSize += size;\n}\n\nexport function PeekQueueValue(container: QueueContainer>): T {\n assert('_queue' in container && '_queueTotalSize' in container);\n assert(container._queue.length > 0);\n\n const pair = container._queue.peek();\n return pair.value;\n}\n\nexport function ResetQueue(container: QueueContainer) {\n assert('_queue' in container && '_queueTotalSize' in container);\n\n container._queue = new SimpleQueue();\n container._queueTotalSize = 0;\n}\n","export function CreateArrayFromList(elements: T): T {\n // We use arrays to represent lists, so this is basically a no-op.\n // Do a slice though just in case we happen to depend on the unique-ness.\n return elements.slice() as T;\n}\n\nexport function CopyDataBlockBytes(dest: ArrayBuffer,\n destOffset: number,\n src: ArrayBuffer,\n srcOffset: number,\n n: number) {\n new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset);\n}\n\n// Not implemented correctly\nexport function TransferArrayBuffer(O: T): T {\n return O;\n}\n\n// Not implemented correctly\nexport function IsDetachedBuffer(O: ArrayBufferLike): boolean { // eslint-disable-line @typescript-eslint/no-unused-vars\n return false;\n}\n","import assert from '../../stub/assert';\nimport { SimpleQueue } from '../simple-queue';\nimport { ResetQueue } from '../abstract-ops/queue-with-sizes';\nimport {\n ReadableStreamAddReadRequest,\n ReadableStreamFulfillReadRequest,\n ReadableStreamGetNumReadRequests,\n ReadableStreamHasDefaultReader,\n ReadRequest\n} from './default-reader';\nimport {\n ReadableStreamAddReadIntoRequest,\n ReadableStreamFulfillReadIntoRequest,\n ReadableStreamGetNumReadIntoRequests,\n ReadableStreamHasBYOBReader,\n ReadIntoRequest\n} from './byob-reader';\nimport NumberIsInteger from '../../stub/number-isinteger';\nimport {\n IsReadableStreamLocked,\n ReadableByteStream,\n ReadableStreamClose,\n ReadableStreamError\n} from '../readable-stream';\nimport { ValidatedUnderlyingByteSource } from './underlying-source';\nimport { typeIsObject } from '../helpers/miscellaneous';\nimport { CopyDataBlockBytes, IsDetachedBuffer, TransferArrayBuffer } from '../abstract-ops/ecmascript';\nimport { CancelSteps, PullSteps } from '../abstract-ops/internal-methods';\nimport { IsFiniteNonNegativeNumber } from '../abstract-ops/miscellaneous';\nimport { promiseResolvedWith, uponPromise } from '../helpers/webidl';\nimport { assertRequiredArgument, convertUnsignedLongLongWithEnforceRange } from '../validators/basic';\n\n/**\n * A pull-into request in a {@link ReadableByteStreamController}.\n *\n * @public\n */\nexport class ReadableStreamBYOBRequest {\n /** @internal */\n _associatedReadableByteStreamController!: ReadableByteStreamController;\n /** @internal */\n _view!: ArrayBufferView | null;\n\n private constructor() {\n throw new TypeError('Illegal constructor');\n }\n\n /**\n * Returns the view for writing in to, or `null` if the BYOB request has already been responded to.\n */\n get view(): ArrayBufferView | null {\n if (!IsReadableStreamBYOBRequest(this)) {\n throw byobRequestBrandCheckException('view');\n }\n\n return this._view;\n }\n\n /**\n * Indicates to the associated readable byte stream that `bytesWritten` bytes were written into\n * {@link ReadableStreamBYOBRequest.view | view}, causing the result be surfaced to the consumer.\n *\n * After this method is called, {@link ReadableStreamBYOBRequest.view | view} will be transferred and no longer\n * modifiable.\n */\n respond(bytesWritten: number): void;\n respond(bytesWritten: number | undefined): void {\n if (!IsReadableStreamBYOBRequest(this)) {\n throw byobRequestBrandCheckException('respond');\n }\n assertRequiredArgument(bytesWritten, 1, 'respond');\n bytesWritten = convertUnsignedLongLongWithEnforceRange(bytesWritten, 'First parameter');\n\n if (this._associatedReadableByteStreamController === undefined) {\n throw new TypeError('This BYOB request has been invalidated');\n }\n\n if (IsDetachedBuffer(this._view!.buffer)) {\n throw new TypeError(`The BYOB request's buffer has been detached and so cannot be used as a response`);\n }\n\n assert(this._view!.byteLength > 0);\n assert(this._view!.buffer.byteLength > 0);\n\n ReadableByteStreamControllerRespond(this._associatedReadableByteStreamController, bytesWritten);\n }\n\n /**\n * Indicates to the associated readable byte stream that instead of writing into\n * {@link ReadableStreamBYOBRequest.view | view}, the underlying byte source is providing a new `ArrayBufferView`,\n * which will be given to the consumer of the readable byte stream.\n *\n * After this method is called, `view` will be transferred and no longer modifiable.\n */\n respondWithNewView(view: ArrayBufferView): void;\n respondWithNewView(view: ArrayBufferView | undefined): void {\n if (!IsReadableStreamBYOBRequest(this)) {\n throw byobRequestBrandCheckException('respondWithNewView');\n }\n assertRequiredArgument(view, 1, 'respondWithNewView');\n\n if (!ArrayBuffer.isView(view)) {\n throw new TypeError('You can only respond with array buffer views');\n }\n if (view.byteLength === 0) {\n throw new TypeError('chunk must have non-zero byteLength');\n }\n if (view.buffer.byteLength === 0) {\n throw new TypeError(`chunk's buffer must have non-zero byteLength`);\n }\n\n if (this._associatedReadableByteStreamController === undefined) {\n throw new TypeError('This BYOB request has been invalidated');\n }\n\n ReadableByteStreamControllerRespondWithNewView(this._associatedReadableByteStreamController, view);\n }\n}\n\nObject.defineProperties(ReadableStreamBYOBRequest.prototype, {\n respond: { enumerable: true },\n respondWithNewView: { enumerable: true },\n view: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(ReadableStreamBYOBRequest.prototype, Symbol.toStringTag, {\n value: 'ReadableStreamBYOBRequest',\n configurable: true\n });\n}\n\ninterface ArrayBufferViewConstructor {\n new(buffer: ArrayBufferLike, byteOffset: number, length?: number): T;\n\n readonly prototype: T;\n readonly BYTES_PER_ELEMENT: number;\n}\n\ninterface ByteQueueElement {\n buffer: ArrayBufferLike;\n byteOffset: number;\n byteLength: number;\n}\n\ntype PullIntoDescriptor =\n DefaultPullIntoDescriptor\n | BYOBPullIntoDescriptor;\n\ninterface DefaultPullIntoDescriptor {\n buffer: ArrayBufferLike;\n byteOffset: number;\n byteLength: number;\n bytesFilled: number;\n elementSize: number;\n viewConstructor: ArrayBufferViewConstructor;\n readerType: 'default';\n}\n\ninterface BYOBPullIntoDescriptor {\n buffer: ArrayBufferLike;\n byteOffset: number;\n byteLength: number;\n bytesFilled: number;\n elementSize: number;\n viewConstructor: ArrayBufferViewConstructor;\n readerType: 'byob';\n}\n\n/**\n * Allows control of a {@link ReadableStream | readable byte stream}'s state and internal queue.\n *\n * @public\n */\nexport class ReadableByteStreamController {\n /** @internal */\n _controlledReadableByteStream!: ReadableByteStream;\n /** @internal */\n _queue!: SimpleQueue;\n /** @internal */\n _queueTotalSize!: number;\n /** @internal */\n _started!: boolean;\n /** @internal */\n _closeRequested!: boolean;\n /** @internal */\n _pullAgain!: boolean;\n /** @internal */\n _pulling !: boolean;\n /** @internal */\n _strategyHWM!: number;\n /** @internal */\n _pullAlgorithm!: () => Promise;\n /** @internal */\n _cancelAlgorithm!: (reason: any) => Promise;\n /** @internal */\n _autoAllocateChunkSize: number | undefined;\n /** @internal */\n _byobRequest: ReadableStreamBYOBRequest | null;\n /** @internal */\n _pendingPullIntos!: SimpleQueue;\n\n private constructor() {\n throw new TypeError('Illegal constructor');\n }\n\n /**\n * Returns the current BYOB pull request, or `null` if there isn't one.\n */\n get byobRequest(): ReadableStreamBYOBRequest | null {\n if (!IsReadableByteStreamController(this)) {\n throw byteStreamControllerBrandCheckException('byobRequest');\n }\n\n if (this._byobRequest === null && this._pendingPullIntos.length > 0) {\n const firstDescriptor = this._pendingPullIntos.peek();\n const view = new Uint8Array(firstDescriptor.buffer,\n firstDescriptor.byteOffset + firstDescriptor.bytesFilled,\n firstDescriptor.byteLength - firstDescriptor.bytesFilled);\n\n const byobRequest: ReadableStreamBYOBRequest = Object.create(ReadableStreamBYOBRequest.prototype);\n SetUpReadableStreamBYOBRequest(byobRequest, this, view);\n this._byobRequest = byobRequest;\n }\n\n return this._byobRequest;\n }\n\n /**\n * Returns the desired size to fill the controlled stream's internal queue. It can be negative, if the queue is\n * over-full. An underlying byte source ought to use this information to determine when and how to apply backpressure.\n */\n get desiredSize(): number | null {\n if (!IsReadableByteStreamController(this)) {\n throw byteStreamControllerBrandCheckException('desiredSize');\n }\n\n return ReadableByteStreamControllerGetDesiredSize(this);\n }\n\n /**\n * Closes the controlled readable stream. Consumers will still be able to read any previously-enqueued chunks from\n * the stream, but once those are read, the stream will become closed.\n */\n close(): void {\n if (!IsReadableByteStreamController(this)) {\n throw byteStreamControllerBrandCheckException('close');\n }\n\n if (this._closeRequested) {\n throw new TypeError('The stream has already been closed; do not close it again!');\n }\n\n const state = this._controlledReadableByteStream._state;\n if (state !== 'readable') {\n throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`);\n }\n\n ReadableByteStreamControllerClose(this);\n }\n\n /**\n * Enqueues the given chunk chunk in the controlled readable stream.\n * The chunk has to be an `ArrayBufferView` instance, or else a `TypeError` will be thrown.\n */\n enqueue(chunk: ArrayBufferView): void;\n enqueue(chunk: ArrayBufferView | undefined): void {\n if (!IsReadableByteStreamController(this)) {\n throw byteStreamControllerBrandCheckException('enqueue');\n }\n\n assertRequiredArgument(chunk, 1, 'enqueue');\n if (!ArrayBuffer.isView(chunk)) {\n throw new TypeError('chunk must be an array buffer view');\n }\n if (chunk.byteLength === 0) {\n throw new TypeError('chunk must have non-zero byteLength');\n }\n if (chunk.buffer.byteLength === 0) {\n throw new TypeError(`chunk's buffer must have non-zero byteLength`);\n }\n\n if (this._closeRequested) {\n throw new TypeError('stream is closed or draining');\n }\n\n const state = this._controlledReadableByteStream._state;\n if (state !== 'readable') {\n throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`);\n }\n\n ReadableByteStreamControllerEnqueue(this, chunk);\n }\n\n /**\n * Errors the controlled readable stream, making all future interactions with it fail with the given error `e`.\n */\n error(e: any = undefined): void {\n if (!IsReadableByteStreamController(this)) {\n throw byteStreamControllerBrandCheckException('error');\n }\n\n ReadableByteStreamControllerError(this, e);\n }\n\n /** @internal */\n [CancelSteps](reason: any): Promise {\n if (this._pendingPullIntos.length > 0) {\n const firstDescriptor = this._pendingPullIntos.peek();\n firstDescriptor.bytesFilled = 0;\n }\n\n ResetQueue(this);\n\n const result = this._cancelAlgorithm(reason);\n ReadableByteStreamControllerClearAlgorithms(this);\n return result;\n }\n\n /** @internal */\n [PullSteps](readRequest: ReadRequest): void {\n const stream = this._controlledReadableByteStream;\n assert(ReadableStreamHasDefaultReader(stream));\n\n if (this._queueTotalSize > 0) {\n assert(ReadableStreamGetNumReadRequests(stream) === 0);\n\n const entry = this._queue.shift()!;\n this._queueTotalSize -= entry.byteLength;\n\n ReadableByteStreamControllerHandleQueueDrain(this);\n\n const view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength);\n\n readRequest._chunkSteps(view);\n return;\n }\n\n const autoAllocateChunkSize = this._autoAllocateChunkSize;\n if (autoAllocateChunkSize !== undefined) {\n let buffer: ArrayBuffer;\n try {\n buffer = new ArrayBuffer(autoAllocateChunkSize);\n } catch (bufferE) {\n readRequest._errorSteps(bufferE);\n return;\n }\n\n const pullIntoDescriptor: DefaultPullIntoDescriptor = {\n buffer,\n byteOffset: 0,\n byteLength: autoAllocateChunkSize,\n bytesFilled: 0,\n elementSize: 1,\n viewConstructor: Uint8Array,\n readerType: 'default'\n };\n\n this._pendingPullIntos.push(pullIntoDescriptor);\n }\n\n ReadableStreamAddReadRequest(stream, readRequest);\n ReadableByteStreamControllerCallPullIfNeeded(this);\n }\n}\n\nObject.defineProperties(ReadableByteStreamController.prototype, {\n close: { enumerable: true },\n enqueue: { enumerable: true },\n error: { enumerable: true },\n byobRequest: { enumerable: true },\n desiredSize: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(ReadableByteStreamController.prototype, Symbol.toStringTag, {\n value: 'ReadableByteStreamController',\n configurable: true\n });\n}\n\n// Abstract operations for the ReadableByteStreamController.\n\nexport function IsReadableByteStreamController(x: any): x is ReadableByteStreamController {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_controlledReadableByteStream')) {\n return false;\n }\n\n return true;\n}\n\nfunction IsReadableStreamBYOBRequest(x: any): x is ReadableStreamBYOBRequest {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_associatedReadableByteStreamController')) {\n return false;\n }\n\n return true;\n}\n\nfunction ReadableByteStreamControllerCallPullIfNeeded(controller: ReadableByteStreamController): void {\n const shouldPull = ReadableByteStreamControllerShouldCallPull(controller);\n if (!shouldPull) {\n return;\n }\n\n if (controller._pulling) {\n controller._pullAgain = true;\n return;\n }\n\n assert(!controller._pullAgain);\n\n controller._pulling = true;\n\n // TODO: Test controller argument\n const pullPromise = controller._pullAlgorithm();\n uponPromise(\n pullPromise,\n () => {\n controller._pulling = false;\n\n if (controller._pullAgain) {\n controller._pullAgain = false;\n ReadableByteStreamControllerCallPullIfNeeded(controller);\n }\n },\n e => {\n ReadableByteStreamControllerError(controller, e);\n }\n );\n}\n\nfunction ReadableByteStreamControllerClearPendingPullIntos(controller: ReadableByteStreamController) {\n ReadableByteStreamControllerInvalidateBYOBRequest(controller);\n controller._pendingPullIntos = new SimpleQueue();\n}\n\nfunction ReadableByteStreamControllerCommitPullIntoDescriptor(stream: ReadableByteStream,\n pullIntoDescriptor: PullIntoDescriptor) {\n assert(stream._state !== 'errored');\n\n let done = false;\n if (stream._state === 'closed') {\n assert(pullIntoDescriptor.bytesFilled === 0);\n done = true;\n }\n\n const filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor);\n if (pullIntoDescriptor.readerType === 'default') {\n ReadableStreamFulfillReadRequest(stream, filledView as unknown as Uint8Array, done);\n } else {\n assert(pullIntoDescriptor.readerType === 'byob');\n ReadableStreamFulfillReadIntoRequest(stream, filledView, done);\n }\n}\n\nfunction ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor: PullIntoDescriptor): T {\n const bytesFilled = pullIntoDescriptor.bytesFilled;\n const elementSize = pullIntoDescriptor.elementSize;\n\n assert(bytesFilled <= pullIntoDescriptor.byteLength);\n assert(bytesFilled % elementSize === 0);\n\n return new pullIntoDescriptor.viewConstructor(\n pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize) as T;\n}\n\nfunction ReadableByteStreamControllerEnqueueChunkToQueue(controller: ReadableByteStreamController,\n buffer: ArrayBufferLike,\n byteOffset: number,\n byteLength: number) {\n controller._queue.push({ buffer, byteOffset, byteLength });\n controller._queueTotalSize += byteLength;\n}\n\nfunction ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller: ReadableByteStreamController,\n pullIntoDescriptor: PullIntoDescriptor) {\n const elementSize = pullIntoDescriptor.elementSize;\n\n const currentAlignedBytes = pullIntoDescriptor.bytesFilled - pullIntoDescriptor.bytesFilled % elementSize;\n\n const maxBytesToCopy = Math.min(controller._queueTotalSize,\n pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled);\n const maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy;\n const maxAlignedBytes = maxBytesFilled - maxBytesFilled % elementSize;\n\n let totalBytesToCopyRemaining = maxBytesToCopy;\n let ready = false;\n if (maxAlignedBytes > currentAlignedBytes) {\n totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled;\n ready = true;\n }\n\n const queue = controller._queue;\n\n while (totalBytesToCopyRemaining > 0) {\n const headOfQueue = queue.peek();\n\n const bytesToCopy = Math.min(totalBytesToCopyRemaining, headOfQueue.byteLength);\n\n const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;\n CopyDataBlockBytes(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy);\n\n if (headOfQueue.byteLength === bytesToCopy) {\n queue.shift();\n } else {\n headOfQueue.byteOffset += bytesToCopy;\n headOfQueue.byteLength -= bytesToCopy;\n }\n controller._queueTotalSize -= bytesToCopy;\n\n ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor);\n\n totalBytesToCopyRemaining -= bytesToCopy;\n }\n\n if (!ready) {\n assert(controller._queueTotalSize === 0);\n assert(pullIntoDescriptor.bytesFilled > 0);\n assert(pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize);\n }\n\n return ready;\n}\n\nfunction ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller: ReadableByteStreamController,\n size: number,\n pullIntoDescriptor: PullIntoDescriptor) {\n assert(controller._pendingPullIntos.length === 0 || controller._pendingPullIntos.peek() === pullIntoDescriptor);\n\n ReadableByteStreamControllerInvalidateBYOBRequest(controller);\n pullIntoDescriptor.bytesFilled += size;\n}\n\nfunction ReadableByteStreamControllerHandleQueueDrain(controller: ReadableByteStreamController) {\n assert(controller._controlledReadableByteStream._state === 'readable');\n\n if (controller._queueTotalSize === 0 && controller._closeRequested) {\n ReadableByteStreamControllerClearAlgorithms(controller);\n ReadableStreamClose(controller._controlledReadableByteStream);\n } else {\n ReadableByteStreamControllerCallPullIfNeeded(controller);\n }\n}\n\nfunction ReadableByteStreamControllerInvalidateBYOBRequest(controller: ReadableByteStreamController) {\n if (controller._byobRequest === null) {\n return;\n }\n\n controller._byobRequest._associatedReadableByteStreamController = undefined!;\n controller._byobRequest._view = null!;\n controller._byobRequest = null;\n}\n\nfunction ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller: ReadableByteStreamController) {\n assert(!controller._closeRequested);\n\n while (controller._pendingPullIntos.length > 0) {\n if (controller._queueTotalSize === 0) {\n return;\n }\n\n const pullIntoDescriptor = controller._pendingPullIntos.peek();\n\n if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)) {\n ReadableByteStreamControllerShiftPendingPullInto(controller);\n\n ReadableByteStreamControllerCommitPullIntoDescriptor(\n controller._controlledReadableByteStream,\n pullIntoDescriptor\n );\n }\n }\n}\n\nexport function ReadableByteStreamControllerPullInto(\n controller: ReadableByteStreamController,\n view: T,\n readIntoRequest: ReadIntoRequest\n): void {\n const stream = controller._controlledReadableByteStream;\n\n let elementSize = 1;\n if (view.constructor !== DataView) {\n elementSize = (view.constructor as ArrayBufferViewConstructor).BYTES_PER_ELEMENT;\n }\n\n const ctor = view.constructor as ArrayBufferViewConstructor;\n\n const buffer = TransferArrayBuffer(view.buffer);\n const pullIntoDescriptor: BYOBPullIntoDescriptor = {\n buffer,\n byteOffset: view.byteOffset,\n byteLength: view.byteLength,\n bytesFilled: 0,\n elementSize,\n viewConstructor: ctor,\n readerType: 'byob'\n };\n\n if (controller._pendingPullIntos.length > 0) {\n controller._pendingPullIntos.push(pullIntoDescriptor);\n\n // No ReadableByteStreamControllerCallPullIfNeeded() call since:\n // - No change happens on desiredSize\n // - The source has already been notified of that there's at least 1 pending read(view)\n\n ReadableStreamAddReadIntoRequest(stream, readIntoRequest);\n return;\n }\n\n if (stream._state === 'closed') {\n const emptyView = new ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0);\n readIntoRequest._closeSteps(emptyView);\n return;\n }\n\n if (controller._queueTotalSize > 0) {\n if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)) {\n const filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor);\n\n ReadableByteStreamControllerHandleQueueDrain(controller);\n\n readIntoRequest._chunkSteps(filledView);\n return;\n }\n\n if (controller._closeRequested) {\n const e = new TypeError('Insufficient bytes to fill elements in the given buffer');\n ReadableByteStreamControllerError(controller, e);\n\n readIntoRequest._errorSteps(e);\n return;\n }\n }\n\n controller._pendingPullIntos.push(pullIntoDescriptor);\n\n ReadableStreamAddReadIntoRequest(stream, readIntoRequest);\n ReadableByteStreamControllerCallPullIfNeeded(controller);\n}\n\nfunction ReadableByteStreamControllerRespondInClosedState(controller: ReadableByteStreamController,\n firstDescriptor: PullIntoDescriptor) {\n firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer);\n\n assert(firstDescriptor.bytesFilled === 0);\n\n const stream = controller._controlledReadableByteStream;\n if (ReadableStreamHasBYOBReader(stream)) {\n while (ReadableStreamGetNumReadIntoRequests(stream) > 0) {\n const pullIntoDescriptor = ReadableByteStreamControllerShiftPendingPullInto(controller);\n ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor);\n }\n }\n}\n\nfunction ReadableByteStreamControllerRespondInReadableState(controller: ReadableByteStreamController,\n bytesWritten: number,\n pullIntoDescriptor: PullIntoDescriptor) {\n if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength) {\n throw new RangeError('bytesWritten out of range');\n }\n\n ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor);\n\n if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize) {\n // TODO: Figure out whether we should detach the buffer or not here.\n return;\n }\n\n ReadableByteStreamControllerShiftPendingPullInto(controller);\n\n const remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize;\n if (remainderSize > 0) {\n const end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;\n const remainder = pullIntoDescriptor.buffer.slice(end - remainderSize, end);\n ReadableByteStreamControllerEnqueueChunkToQueue(controller, remainder, 0, remainder.byteLength);\n }\n\n pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer);\n pullIntoDescriptor.bytesFilled -= remainderSize;\n ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableByteStream, pullIntoDescriptor);\n\n ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);\n}\n\nfunction ReadableByteStreamControllerRespondInternal(controller: ReadableByteStreamController, bytesWritten: number) {\n const firstDescriptor = controller._pendingPullIntos.peek();\n\n const state = controller._controlledReadableByteStream._state;\n\n if (state === 'closed') {\n if (bytesWritten !== 0) {\n throw new TypeError('bytesWritten must be 0 when calling respond() on a closed stream');\n }\n\n ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor);\n } else {\n assert(state === 'readable');\n\n ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor);\n }\n\n ReadableByteStreamControllerCallPullIfNeeded(controller);\n}\n\nfunction ReadableByteStreamControllerShiftPendingPullInto(controller: ReadableByteStreamController): PullIntoDescriptor {\n const descriptor = controller._pendingPullIntos.shift()!;\n ReadableByteStreamControllerInvalidateBYOBRequest(controller);\n return descriptor;\n}\n\nfunction ReadableByteStreamControllerShouldCallPull(controller: ReadableByteStreamController): boolean {\n const stream = controller._controlledReadableByteStream;\n\n if (stream._state !== 'readable') {\n return false;\n }\n\n if (controller._closeRequested) {\n return false;\n }\n\n if (!controller._started) {\n return false;\n }\n\n if (ReadableStreamHasDefaultReader(stream) && ReadableStreamGetNumReadRequests(stream) > 0) {\n return true;\n }\n\n if (ReadableStreamHasBYOBReader(stream) && ReadableStreamGetNumReadIntoRequests(stream) > 0) {\n return true;\n }\n\n const desiredSize = ReadableByteStreamControllerGetDesiredSize(controller);\n assert(desiredSize !== null);\n if (desiredSize! > 0) {\n return true;\n }\n\n return false;\n}\n\nfunction ReadableByteStreamControllerClearAlgorithms(controller: ReadableByteStreamController) {\n controller._pullAlgorithm = undefined!;\n controller._cancelAlgorithm = undefined!;\n}\n\n// A client of ReadableByteStreamController may use these functions directly to bypass state check.\n\nfunction ReadableByteStreamControllerClose(controller: ReadableByteStreamController) {\n const stream = controller._controlledReadableByteStream;\n\n if (controller._closeRequested || stream._state !== 'readable') {\n return;\n }\n\n if (controller._queueTotalSize > 0) {\n controller._closeRequested = true;\n\n return;\n }\n\n if (controller._pendingPullIntos.length > 0) {\n const firstPendingPullInto = controller._pendingPullIntos.peek();\n if (firstPendingPullInto.bytesFilled > 0) {\n const e = new TypeError('Insufficient bytes to fill elements in the given buffer');\n ReadableByteStreamControllerError(controller, e);\n\n throw e;\n }\n }\n\n ReadableByteStreamControllerClearAlgorithms(controller);\n ReadableStreamClose(stream);\n}\n\nfunction ReadableByteStreamControllerEnqueue(controller: ReadableByteStreamController, chunk: ArrayBufferView) {\n const stream = controller._controlledReadableByteStream;\n\n if (controller._closeRequested || stream._state !== 'readable') {\n return;\n }\n\n const buffer = chunk.buffer;\n const byteOffset = chunk.byteOffset;\n const byteLength = chunk.byteLength;\n const transferredBuffer = TransferArrayBuffer(buffer);\n\n if (ReadableStreamHasDefaultReader(stream)) {\n if (ReadableStreamGetNumReadRequests(stream) === 0) {\n ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);\n } else {\n assert(controller._queue.length === 0);\n\n const transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength);\n ReadableStreamFulfillReadRequest(stream, transferredView, false);\n }\n } else if (ReadableStreamHasBYOBReader(stream)) {\n // TODO: Ideally in this branch detaching should happen only if the buffer is not consumed fully.\n ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);\n ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);\n } else {\n assert(!IsReadableStreamLocked(stream));\n ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength);\n }\n\n ReadableByteStreamControllerCallPullIfNeeded(controller);\n}\n\nfunction ReadableByteStreamControllerError(controller: ReadableByteStreamController, e: any) {\n const stream = controller._controlledReadableByteStream;\n\n if (stream._state !== 'readable') {\n return;\n }\n\n ReadableByteStreamControllerClearPendingPullIntos(controller);\n\n ResetQueue(controller);\n ReadableByteStreamControllerClearAlgorithms(controller);\n ReadableStreamError(stream, e);\n}\n\nfunction ReadableByteStreamControllerGetDesiredSize(controller: ReadableByteStreamController): number | null {\n const state = controller._controlledReadableByteStream._state;\n\n if (state === 'errored') {\n return null;\n }\n if (state === 'closed') {\n return 0;\n }\n\n return controller._strategyHWM - controller._queueTotalSize;\n}\n\nfunction ReadableByteStreamControllerRespond(controller: ReadableByteStreamController, bytesWritten: number) {\n bytesWritten = Number(bytesWritten);\n if (!IsFiniteNonNegativeNumber(bytesWritten)) {\n throw new RangeError('bytesWritten must be a finite');\n }\n\n assert(controller._pendingPullIntos.length > 0);\n\n ReadableByteStreamControllerRespondInternal(controller, bytesWritten);\n}\n\nfunction ReadableByteStreamControllerRespondWithNewView(controller: ReadableByteStreamController,\n view: ArrayBufferView) {\n assert(controller._pendingPullIntos.length > 0);\n\n const firstDescriptor = controller._pendingPullIntos.peek();\n\n if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) {\n throw new RangeError('The region specified by view does not match byobRequest');\n }\n if (firstDescriptor.byteLength !== view.byteLength) {\n throw new RangeError('The buffer of view has different capacity than byobRequest');\n }\n\n firstDescriptor.buffer = view.buffer;\n\n ReadableByteStreamControllerRespondInternal(controller, view.byteLength);\n}\n\nexport function SetUpReadableByteStreamController(stream: ReadableByteStream,\n controller: ReadableByteStreamController,\n startAlgorithm: () => void | PromiseLike,\n pullAlgorithm: () => Promise,\n cancelAlgorithm: (reason: any) => Promise,\n highWaterMark: number,\n autoAllocateChunkSize: number | undefined) {\n assert(stream._readableStreamController === undefined);\n if (autoAllocateChunkSize !== undefined) {\n assert(NumberIsInteger(autoAllocateChunkSize));\n assert(autoAllocateChunkSize > 0);\n }\n\n controller._controlledReadableByteStream = stream;\n\n controller._pullAgain = false;\n controller._pulling = false;\n\n controller._byobRequest = null;\n\n // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly.\n controller._queue = controller._queueTotalSize = undefined!;\n ResetQueue(controller);\n\n controller._closeRequested = false;\n controller._started = false;\n\n controller._strategyHWM = highWaterMark;\n\n controller._pullAlgorithm = pullAlgorithm;\n controller._cancelAlgorithm = cancelAlgorithm;\n\n controller._autoAllocateChunkSize = autoAllocateChunkSize;\n\n controller._pendingPullIntos = new SimpleQueue();\n\n stream._readableStreamController = controller;\n\n const startResult = startAlgorithm();\n uponPromise(\n promiseResolvedWith(startResult),\n () => {\n controller._started = true;\n\n assert(!controller._pulling);\n assert(!controller._pullAgain);\n\n ReadableByteStreamControllerCallPullIfNeeded(controller);\n },\n r => {\n ReadableByteStreamControllerError(controller, r);\n }\n );\n}\n\nexport function SetUpReadableByteStreamControllerFromUnderlyingSource(\n stream: ReadableByteStream,\n underlyingByteSource: ValidatedUnderlyingByteSource,\n highWaterMark: number\n) {\n const controller: ReadableByteStreamController = Object.create(ReadableByteStreamController.prototype);\n\n let startAlgorithm: () => void | PromiseLike = () => undefined;\n let pullAlgorithm: () => Promise = () => promiseResolvedWith(undefined);\n let cancelAlgorithm: (reason: any) => Promise = () => promiseResolvedWith(undefined);\n\n if (underlyingByteSource.start !== undefined) {\n startAlgorithm = () => underlyingByteSource.start!(controller);\n }\n if (underlyingByteSource.pull !== undefined) {\n pullAlgorithm = () => underlyingByteSource.pull!(controller);\n }\n if (underlyingByteSource.cancel !== undefined) {\n cancelAlgorithm = reason => underlyingByteSource.cancel!(reason);\n }\n\n const autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize;\n\n SetUpReadableByteStreamController(\n stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize\n );\n}\n\nfunction SetUpReadableStreamBYOBRequest(request: ReadableStreamBYOBRequest,\n controller: ReadableByteStreamController,\n view: ArrayBufferView) {\n assert(IsReadableByteStreamController(controller));\n assert(typeof view === 'object');\n assert(ArrayBuffer.isView(view));\n assert(!IsDetachedBuffer(view.buffer));\n request._associatedReadableByteStreamController = controller;\n request._view = view;\n}\n\n// Helper functions for the ReadableStreamBYOBRequest.\n\nfunction byobRequestBrandCheckException(name: string): TypeError {\n return new TypeError(\n `ReadableStreamBYOBRequest.prototype.${name} can only be used on a ReadableStreamBYOBRequest`);\n}\n\n// Helper functions for the ReadableByteStreamController.\n\nfunction byteStreamControllerBrandCheckException(name: string): TypeError {\n return new TypeError(\n `ReadableByteStreamController.prototype.${name} can only be used on a ReadableByteStreamController`);\n}\n","import assert from '../../stub/assert';\nimport { SimpleQueue } from '../simple-queue';\nimport {\n ReadableStreamReaderGenericCancel,\n ReadableStreamReaderGenericInitialize,\n ReadableStreamReaderGenericRelease,\n readerLockException\n} from './generic-reader';\nimport { IsReadableStreamLocked, ReadableByteStream, ReadableStream } from '../readable-stream';\nimport {\n IsReadableByteStreamController,\n ReadableByteStreamController,\n ReadableByteStreamControllerPullInto\n} from './byte-stream-controller';\nimport { typeIsObject } from '../helpers/miscellaneous';\nimport { newPromise, promiseRejectedWith } from '../helpers/webidl';\nimport { assertRequiredArgument } from '../validators/basic';\nimport { assertReadableStream } from '../validators/readable-stream';\n\n/**\n * A result returned by {@link ReadableStreamBYOBReader.read}.\n *\n * @public\n */\nexport type ReadableStreamBYOBReadResult = {\n done: boolean;\n value: T;\n};\n\n// Abstract operations for the ReadableStream.\n\nexport function AcquireReadableStreamBYOBReader(stream: ReadableStream): ReadableStreamBYOBReader {\n return new ReadableStreamBYOBReader(stream);\n}\n\n// ReadableStream API exposed for controllers.\n\nexport function ReadableStreamAddReadIntoRequest(stream: ReadableByteStream,\n readIntoRequest: ReadIntoRequest): void {\n assert(IsReadableStreamBYOBReader(stream._reader));\n assert(stream._state === 'readable' || stream._state === 'closed');\n\n (stream._reader! as ReadableStreamBYOBReader)._readIntoRequests.push(readIntoRequest);\n}\n\nexport function ReadableStreamFulfillReadIntoRequest(stream: ReadableByteStream,\n chunk: ArrayBufferView,\n done: boolean) {\n const reader = stream._reader as ReadableStreamBYOBReader;\n\n assert(reader._readIntoRequests.length > 0);\n\n const readIntoRequest = reader._readIntoRequests.shift()!;\n if (done) {\n readIntoRequest._closeSteps(chunk);\n } else {\n readIntoRequest._chunkSteps(chunk);\n }\n}\n\nexport function ReadableStreamGetNumReadIntoRequests(stream: ReadableByteStream): number {\n return (stream._reader as ReadableStreamBYOBReader)._readIntoRequests.length;\n}\n\nexport function ReadableStreamHasBYOBReader(stream: ReadableByteStream): boolean {\n const reader = stream._reader;\n\n if (reader === undefined) {\n return false;\n }\n\n if (!IsReadableStreamBYOBReader(reader)) {\n return false;\n }\n\n return true;\n}\n\n// Readers\n\nexport interface ReadIntoRequest {\n _chunkSteps(chunk: T): void;\n\n _closeSteps(chunk: T): void;\n\n _errorSteps(e: any): void;\n}\n\n/**\n * A BYOB reader vended by a {@link ReadableStream}.\n *\n * @public\n */\nexport class ReadableStreamBYOBReader {\n /** @internal */\n _ownerReadableStream!: ReadableByteStream;\n /** @internal */\n _closedPromise!: Promise;\n /** @internal */\n _closedPromise_resolve?: (value?: undefined) => void;\n /** @internal */\n _closedPromise_reject?: (reason: any) => void;\n /** @internal */\n _readIntoRequests: SimpleQueue>;\n\n constructor(stream: ReadableByteStream) {\n assertRequiredArgument(stream, 1, 'ReadableStreamBYOBReader');\n assertReadableStream(stream, 'First parameter');\n\n if (IsReadableStreamLocked(stream)) {\n throw new TypeError('This stream has already been locked for exclusive reading by another reader');\n }\n\n if (!IsReadableByteStreamController(stream._readableStreamController)) {\n throw new TypeError('Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte ' +\n 'source');\n }\n\n ReadableStreamReaderGenericInitialize(this, stream);\n\n this._readIntoRequests = new SimpleQueue();\n }\n\n /**\n * Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the stream ever errors or\n * the reader's lock is released before the stream finishes closing.\n */\n get closed(): Promise {\n if (!IsReadableStreamBYOBReader(this)) {\n return promiseRejectedWith(byobReaderBrandCheckException('closed'));\n }\n\n return this._closedPromise;\n }\n\n /**\n * If the reader is active, behaves the same as {@link ReadableStream.cancel | stream.cancel(reason)}.\n */\n cancel(reason: any = undefined): Promise {\n if (!IsReadableStreamBYOBReader(this)) {\n return promiseRejectedWith(byobReaderBrandCheckException('cancel'));\n }\n\n if (this._ownerReadableStream === undefined) {\n return promiseRejectedWith(readerLockException('cancel'));\n }\n\n return ReadableStreamReaderGenericCancel(this, reason);\n }\n\n /**\n * Attempts to reads bytes into view, and returns a promise resolved with the result.\n *\n * If reading a chunk causes the queue to become empty, more data will be pulled from the underlying source.\n */\n read(view: T): Promise> {\n if (!IsReadableStreamBYOBReader(this)) {\n return promiseRejectedWith(byobReaderBrandCheckException('read'));\n }\n\n if (!ArrayBuffer.isView(view)) {\n return promiseRejectedWith(new TypeError('view must be an array buffer view'));\n }\n if (view.byteLength === 0) {\n return promiseRejectedWith(new TypeError('view must have non-zero byteLength'));\n }\n if (view.buffer.byteLength === 0) {\n return promiseRejectedWith(new TypeError(`view's buffer must have non-zero byteLength`));\n }\n\n if (this._ownerReadableStream === undefined) {\n return promiseRejectedWith(readerLockException('read from'));\n }\n\n let resolvePromise!: (result: ReadableStreamBYOBReadResult) => void;\n let rejectPromise!: (reason: any) => void;\n const promise = newPromise>((resolve, reject) => {\n resolvePromise = resolve;\n rejectPromise = reject;\n });\n const readIntoRequest: ReadIntoRequest = {\n _chunkSteps: chunk => resolvePromise({ value: chunk, done: false }),\n _closeSteps: chunk => resolvePromise({ value: chunk, done: true }),\n _errorSteps: e => rejectPromise(e)\n };\n ReadableStreamBYOBReaderRead(this, view, readIntoRequest);\n return promise;\n }\n\n /**\n * Releases the reader's lock on the corresponding stream. After the lock is released, the reader is no longer active.\n * If the associated stream is errored when the lock is released, the reader will appear errored in the same way\n * from now on; otherwise, the reader will appear closed.\n *\n * A reader's lock cannot be released while it still has a pending read request, i.e., if a promise returned by\n * the reader's {@link ReadableStreamBYOBReader.read | read()} method has not yet been settled. Attempting to\n * do so will throw a `TypeError` and leave the reader locked to the stream.\n */\n releaseLock(): void {\n if (!IsReadableStreamBYOBReader(this)) {\n throw byobReaderBrandCheckException('releaseLock');\n }\n\n if (this._ownerReadableStream === undefined) {\n return;\n }\n\n if (this._readIntoRequests.length > 0) {\n throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled');\n }\n\n ReadableStreamReaderGenericRelease(this);\n }\n}\n\nObject.defineProperties(ReadableStreamBYOBReader.prototype, {\n cancel: { enumerable: true },\n read: { enumerable: true },\n releaseLock: { enumerable: true },\n closed: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(ReadableStreamBYOBReader.prototype, Symbol.toStringTag, {\n value: 'ReadableStreamBYOBReader',\n configurable: true\n });\n}\n\n// Abstract operations for the readers.\n\nexport function IsReadableStreamBYOBReader(x: any): x is ReadableStreamBYOBReader {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_readIntoRequests')) {\n return false;\n }\n\n return true;\n}\n\nfunction ReadableStreamBYOBReaderRead(reader: ReadableStreamBYOBReader,\n view: T,\n readIntoRequest: ReadIntoRequest): void {\n const stream = reader._ownerReadableStream;\n\n assert(stream !== undefined);\n\n stream._disturbed = true;\n\n if (stream._state === 'errored') {\n readIntoRequest._errorSteps(stream._storedError);\n } else {\n ReadableByteStreamControllerPullInto(\n stream._readableStreamController as ReadableByteStreamController,\n view,\n readIntoRequest\n );\n }\n}\n\n// Helper functions for the ReadableStreamBYOBReader.\n\nfunction byobReaderBrandCheckException(name: string): TypeError {\n return new TypeError(\n `ReadableStreamBYOBReader.prototype.${name} can only be used on a ReadableStreamBYOBReader`);\n}\n","import { QueuingStrategy, QueuingStrategySizeCallback } from '../queuing-strategy';\nimport NumberIsNaN from '../../stub/number-isnan';\n\nexport function ExtractHighWaterMark(strategy: QueuingStrategy, defaultHWM: number): number {\n const { highWaterMark } = strategy;\n\n if (highWaterMark === undefined) {\n return defaultHWM;\n }\n\n if (NumberIsNaN(highWaterMark) || highWaterMark < 0) {\n throw new RangeError('Invalid highWaterMark');\n }\n\n return highWaterMark;\n}\n\nexport function ExtractSizeAlgorithm(strategy: QueuingStrategy): QueuingStrategySizeCallback {\n const { size } = strategy;\n\n if (!size) {\n return () => 1;\n }\n\n return size;\n}\n","import { QueuingStrategy, QueuingStrategySizeCallback } from '../queuing-strategy';\nimport { assertDictionary, assertFunction, convertUnrestrictedDouble } from './basic';\n\nexport function convertQueuingStrategy(init: QueuingStrategy | null | undefined,\n context: string): QueuingStrategy {\n assertDictionary(init, context);\n const highWaterMark = init?.highWaterMark;\n const size = init?.size;\n return {\n highWaterMark: highWaterMark === undefined ? undefined : convertUnrestrictedDouble(highWaterMark),\n size: size === undefined ? undefined : convertQueuingStrategySize(size, `${context} has member 'size' that`)\n };\n}\n\nfunction convertQueuingStrategySize(fn: QueuingStrategySizeCallback,\n context: string): QueuingStrategySizeCallback {\n assertFunction(fn, context);\n return chunk => convertUnrestrictedDouble(fn(chunk));\n}\n","import { assertDictionary, assertFunction } from './basic';\nimport { promiseCall, reflectCall } from '../helpers/webidl';\nimport {\n UnderlyingSink,\n UnderlyingSinkAbortCallback,\n UnderlyingSinkCloseCallback,\n UnderlyingSinkStartCallback,\n UnderlyingSinkWriteCallback,\n ValidatedUnderlyingSink\n} from '../writable-stream/underlying-sink';\nimport { WritableStreamDefaultController } from '../writable-stream';\n\nexport function convertUnderlyingSink(original: UnderlyingSink | null,\n context: string): ValidatedUnderlyingSink {\n assertDictionary(original, context);\n const abort = original?.abort;\n const close = original?.close;\n const start = original?.start;\n const type = original?.type;\n const write = original?.write;\n return {\n abort: abort === undefined ?\n undefined :\n convertUnderlyingSinkAbortCallback(abort, original!, `${context} has member 'abort' that`),\n close: close === undefined ?\n undefined :\n convertUnderlyingSinkCloseCallback(close, original!, `${context} has member 'close' that`),\n start: start === undefined ?\n undefined :\n convertUnderlyingSinkStartCallback(start, original!, `${context} has member 'start' that`),\n write: write === undefined ?\n undefined :\n convertUnderlyingSinkWriteCallback(write, original!, `${context} has member 'write' that`),\n type\n };\n}\n\nfunction convertUnderlyingSinkAbortCallback(\n fn: UnderlyingSinkAbortCallback,\n original: UnderlyingSink,\n context: string\n): (reason: any) => Promise {\n assertFunction(fn, context);\n return (reason: any) => promiseCall(fn, original, [reason]);\n}\n\nfunction convertUnderlyingSinkCloseCallback(\n fn: UnderlyingSinkCloseCallback,\n original: UnderlyingSink,\n context: string\n): () => Promise {\n assertFunction(fn, context);\n return () => promiseCall(fn, original, []);\n}\n\nfunction convertUnderlyingSinkStartCallback(\n fn: UnderlyingSinkStartCallback,\n original: UnderlyingSink,\n context: string\n): UnderlyingSinkStartCallback {\n assertFunction(fn, context);\n return (controller: WritableStreamDefaultController) => reflectCall(fn, original, [controller]);\n}\n\nfunction convertUnderlyingSinkWriteCallback(\n fn: UnderlyingSinkWriteCallback,\n original: UnderlyingSink,\n context: string\n): (chunk: W, controller: WritableStreamDefaultController) => Promise {\n assertFunction(fn, context);\n return (chunk: W, controller: WritableStreamDefaultController) => promiseCall(fn, original, [chunk, controller]);\n}\n","import { IsWritableStream, WritableStream } from '../writable-stream';\n\nexport function assertWritableStream(x: unknown, context: string): asserts x is WritableStream {\n if (!IsWritableStream(x)) {\n throw new TypeError(`${context} is not a WritableStream.`);\n }\n}\n","import assert from '../stub/assert';\nimport {\n newPromise,\n promiseRejectedWith,\n promiseResolvedWith,\n setPromiseIsHandledToTrue,\n uponPromise\n} from './helpers/webidl';\nimport {\n DequeueValue,\n EnqueueValueWithSize,\n PeekQueueValue,\n QueuePair,\n ResetQueue\n} from './abstract-ops/queue-with-sizes';\nimport { QueuingStrategy, QueuingStrategySizeCallback } from './queuing-strategy';\nimport { SimpleQueue } from './simple-queue';\nimport { typeIsObject } from './helpers/miscellaneous';\nimport { AbortSteps, ErrorSteps } from './abstract-ops/internal-methods';\nimport { IsNonNegativeNumber } from './abstract-ops/miscellaneous';\nimport { ExtractHighWaterMark, ExtractSizeAlgorithm } from './abstract-ops/queuing-strategy';\nimport { convertQueuingStrategy } from './validators/queuing-strategy';\nimport {\n UnderlyingSink,\n UnderlyingSinkAbortCallback,\n UnderlyingSinkCloseCallback,\n UnderlyingSinkStartCallback,\n UnderlyingSinkWriteCallback,\n ValidatedUnderlyingSink\n} from './writable-stream/underlying-sink';\nimport { assertObject, assertRequiredArgument } from './validators/basic';\nimport { convertUnderlyingSink } from './validators/underlying-sink';\nimport { assertWritableStream } from './validators/writable-stream';\n\ntype WritableStreamState = 'writable' | 'closed' | 'erroring' | 'errored';\n\ninterface WriteOrCloseRequest {\n _resolve: (value?: undefined) => void;\n _reject: (reason: any) => void;\n}\n\ntype WriteRequest = WriteOrCloseRequest;\ntype CloseRequest = WriteOrCloseRequest;\n\ninterface PendingAbortRequest {\n _promise: Promise;\n _resolve: () => void;\n _reject: (reason: any) => void;\n _reason: any;\n _wasAlreadyErroring: boolean;\n}\n\n/**\n * A writable stream represents a destination for data, into which you can write.\n *\n * @public\n */\nclass WritableStream {\n /** @internal */\n _state!: WritableStreamState;\n /** @internal */\n _storedError: any;\n /** @internal */\n _writer: WritableStreamDefaultWriter | undefined;\n /** @internal */\n _writableStreamController!: WritableStreamDefaultController;\n /** @internal */\n _writeRequests!: SimpleQueue;\n /** @internal */\n _inFlightWriteRequest: WriteRequest | undefined;\n /** @internal */\n _closeRequest: CloseRequest | undefined;\n /** @internal */\n _inFlightCloseRequest: CloseRequest | undefined;\n /** @internal */\n _pendingAbortRequest: PendingAbortRequest | undefined;\n /** @internal */\n _backpressure!: boolean;\n\n constructor(underlyingSink?: UnderlyingSink, strategy?: QueuingStrategy);\n constructor(rawUnderlyingSink: UnderlyingSink | null | undefined = {},\n rawStrategy: QueuingStrategy | null | undefined = {}) {\n if (rawUnderlyingSink === undefined) {\n rawUnderlyingSink = null;\n } else {\n assertObject(rawUnderlyingSink, 'First parameter');\n }\n\n const strategy = convertQueuingStrategy(rawStrategy, 'Second parameter');\n const underlyingSink = convertUnderlyingSink(rawUnderlyingSink, 'First parameter');\n\n InitializeWritableStream(this);\n\n const type = underlyingSink.type;\n if (type !== undefined) {\n throw new RangeError('Invalid type is specified');\n }\n\n const sizeAlgorithm = ExtractSizeAlgorithm(strategy);\n const highWaterMark = ExtractHighWaterMark(strategy, 1);\n\n SetUpWritableStreamDefaultControllerFromUnderlyingSink(this, underlyingSink, highWaterMark, sizeAlgorithm);\n }\n\n /**\n * Returns whether or not the writable stream is locked to a writer.\n */\n get locked(): boolean {\n if (!IsWritableStream(this)) {\n throw streamBrandCheckException('locked');\n }\n\n return IsWritableStreamLocked(this);\n }\n\n /**\n * Aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be\n * immediately moved to an errored state, with any queued-up writes discarded. This will also execute any abort\n * mechanism of the underlying sink.\n *\n * The returned promise will fulfill if the stream shuts down successfully, or reject if the underlying sink signaled\n * that there was an error doing so. Additionally, it will reject with a `TypeError` (without attempting to cancel\n * the stream) if the stream is currently locked.\n */\n abort(reason: any = undefined): Promise {\n if (!IsWritableStream(this)) {\n return promiseRejectedWith(streamBrandCheckException('abort'));\n }\n\n if (IsWritableStreamLocked(this)) {\n return promiseRejectedWith(new TypeError('Cannot abort a stream that already has a writer'));\n }\n\n return WritableStreamAbort(this, reason);\n }\n\n /**\n * Closes the stream. The underlying sink will finish processing any previously-written chunks, before invoking its\n * close behavior. During this time any further attempts to write will fail (without erroring the stream).\n *\n * The method returns a promise that will fulfill if all remaining chunks are successfully written and the stream\n * successfully closes, or rejects if an error is encountered during this process. Additionally, it will reject with\n * a `TypeError` (without attempting to cancel the stream) if the stream is currently locked.\n */\n close() {\n if (!IsWritableStream(this)) {\n return promiseRejectedWith(streamBrandCheckException('close'));\n }\n\n if (IsWritableStreamLocked(this)) {\n return promiseRejectedWith(new TypeError('Cannot close a stream that already has a writer'));\n }\n\n if (WritableStreamCloseQueuedOrInFlight(this)) {\n return promiseRejectedWith(new TypeError('Cannot close an already-closing stream'));\n }\n\n return WritableStreamClose(this);\n }\n\n /**\n * Creates a {@link WritableStreamDefaultWriter | writer} and locks the stream to the new writer. While the stream\n * is locked, no other writer can be acquired until this one is released.\n *\n * This functionality is especially useful for creating abstractions that desire the ability to write to a stream\n * without interruption or interleaving. By getting a writer for the stream, you can ensure nobody else can write at\n * the same time, which would cause the resulting written data to be unpredictable and probably useless.\n */\n getWriter(): WritableStreamDefaultWriter {\n if (!IsWritableStream(this)) {\n throw streamBrandCheckException('getWriter');\n }\n\n return AcquireWritableStreamDefaultWriter(this);\n }\n}\n\nObject.defineProperties(WritableStream.prototype, {\n abort: { enumerable: true },\n close: { enumerable: true },\n getWriter: { enumerable: true },\n locked: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(WritableStream.prototype, Symbol.toStringTag, {\n value: 'WritableStream',\n configurable: true\n });\n}\n\nexport {\n AcquireWritableStreamDefaultWriter,\n CreateWritableStream,\n IsWritableStream,\n IsWritableStreamLocked,\n WritableStream,\n WritableStreamAbort,\n WritableStreamDefaultControllerErrorIfNeeded,\n WritableStreamDefaultWriterCloseWithErrorPropagation,\n WritableStreamDefaultWriterRelease,\n WritableStreamDefaultWriterWrite,\n WritableStreamCloseQueuedOrInFlight,\n UnderlyingSink,\n UnderlyingSinkStartCallback,\n UnderlyingSinkWriteCallback,\n UnderlyingSinkCloseCallback,\n UnderlyingSinkAbortCallback\n};\n\n// Abstract operations for the WritableStream.\n\nfunction AcquireWritableStreamDefaultWriter(stream: WritableStream): WritableStreamDefaultWriter {\n return new WritableStreamDefaultWriter(stream);\n}\n\n// Throws if and only if startAlgorithm throws.\nfunction CreateWritableStream(startAlgorithm: () => void | PromiseLike,\n writeAlgorithm: (chunk: W) => Promise,\n closeAlgorithm: () => Promise,\n abortAlgorithm: (reason: any) => Promise,\n highWaterMark = 1,\n sizeAlgorithm: QueuingStrategySizeCallback = () => 1) {\n assert(IsNonNegativeNumber(highWaterMark));\n\n const stream: WritableStream = Object.create(WritableStream.prototype);\n InitializeWritableStream(stream);\n\n const controller: WritableStreamDefaultController = Object.create(WritableStreamDefaultController.prototype);\n\n SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm,\n abortAlgorithm, highWaterMark, sizeAlgorithm);\n return stream;\n}\n\nfunction InitializeWritableStream(stream: WritableStream) {\n stream._state = 'writable';\n\n // The error that will be reported by new method calls once the state becomes errored. Only set when [[state]] is\n // 'erroring' or 'errored'. May be set to an undefined value.\n stream._storedError = undefined;\n\n stream._writer = undefined;\n\n // Initialize to undefined first because the constructor of the controller checks this\n // variable to validate the caller.\n stream._writableStreamController = undefined!;\n\n // This queue is placed here instead of the writer class in order to allow for passing a writer to the next data\n // producer without waiting for the queued writes to finish.\n stream._writeRequests = new SimpleQueue();\n\n // Write requests are removed from _writeRequests when write() is called on the underlying sink. This prevents\n // them from being erroneously rejected on error. If a write() call is in-flight, the request is stored here.\n stream._inFlightWriteRequest = undefined;\n\n // The promise that was returned from writer.close(). Stored here because it may be fulfilled after the writer\n // has been detached.\n stream._closeRequest = undefined;\n\n // Close request is removed from _closeRequest when close() is called on the underlying sink. This prevents it\n // from being erroneously rejected on error. If a close() call is in-flight, the request is stored here.\n stream._inFlightCloseRequest = undefined;\n\n // The promise that was returned from writer.abort(). This may also be fulfilled after the writer has detached.\n stream._pendingAbortRequest = undefined;\n\n // The backpressure signal set by the controller.\n stream._backpressure = false;\n}\n\nfunction IsWritableStream(x: unknown): x is WritableStream {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_writableStreamController')) {\n return false;\n }\n\n return true;\n}\n\nfunction IsWritableStreamLocked(stream: WritableStream): boolean {\n assert(IsWritableStream(stream));\n\n if (stream._writer === undefined) {\n return false;\n }\n\n return true;\n}\n\nfunction WritableStreamAbort(stream: WritableStream, reason: any): Promise {\n const state = stream._state;\n if (state === 'closed' || state === 'errored') {\n return promiseResolvedWith(undefined);\n }\n if (stream._pendingAbortRequest !== undefined) {\n return stream._pendingAbortRequest._promise;\n }\n\n assert(state === 'writable' || state === 'erroring');\n\n let wasAlreadyErroring = false;\n if (state === 'erroring') {\n wasAlreadyErroring = true;\n // reason will not be used, so don't keep a reference to it.\n reason = undefined;\n }\n\n const promise = newPromise((resolve, reject) => {\n stream._pendingAbortRequest = {\n _promise: undefined!,\n _resolve: resolve,\n _reject: reject,\n _reason: reason,\n _wasAlreadyErroring: wasAlreadyErroring\n };\n });\n stream._pendingAbortRequest!._promise = promise;\n\n if (!wasAlreadyErroring) {\n WritableStreamStartErroring(stream, reason);\n }\n\n return promise;\n}\n\nfunction WritableStreamClose(stream: WritableStream): Promise {\n const state = stream._state;\n if (state === 'closed' || state === 'errored') {\n return promiseRejectedWith(new TypeError(\n `The stream (in ${state} state) is not in the writable state and cannot be closed`));\n }\n\n assert(state === 'writable' || state === 'erroring');\n assert(!WritableStreamCloseQueuedOrInFlight(stream));\n\n const promise = newPromise((resolve, reject) => {\n const closeRequest: CloseRequest = {\n _resolve: resolve,\n _reject: reject\n };\n\n stream._closeRequest = closeRequest;\n });\n\n const writer = stream._writer;\n if (writer !== undefined && stream._backpressure && state === 'writable') {\n defaultWriterReadyPromiseResolve(writer);\n }\n\n WritableStreamDefaultControllerClose(stream._writableStreamController);\n\n return promise;\n}\n\n// WritableStream API exposed for controllers.\n\nfunction WritableStreamAddWriteRequest(stream: WritableStream): Promise {\n assert(IsWritableStreamLocked(stream));\n assert(stream._state === 'writable');\n\n const promise = newPromise((resolve, reject) => {\n const writeRequest: WriteRequest = {\n _resolve: resolve,\n _reject: reject\n };\n\n stream._writeRequests.push(writeRequest);\n });\n\n return promise;\n}\n\nfunction WritableStreamDealWithRejection(stream: WritableStream, error: any) {\n const state = stream._state;\n\n if (state === 'writable') {\n WritableStreamStartErroring(stream, error);\n return;\n }\n\n assert(state === 'erroring');\n WritableStreamFinishErroring(stream);\n}\n\nfunction WritableStreamStartErroring(stream: WritableStream, reason: any) {\n assert(stream._storedError === undefined);\n assert(stream._state === 'writable');\n\n const controller = stream._writableStreamController;\n assert(controller !== undefined);\n\n stream._state = 'erroring';\n stream._storedError = reason;\n const writer = stream._writer;\n if (writer !== undefined) {\n WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason);\n }\n\n if (!WritableStreamHasOperationMarkedInFlight(stream) && controller._started) {\n WritableStreamFinishErroring(stream);\n }\n}\n\nfunction WritableStreamFinishErroring(stream: WritableStream) {\n assert(stream._state === 'erroring');\n assert(!WritableStreamHasOperationMarkedInFlight(stream));\n stream._state = 'errored';\n stream._writableStreamController[ErrorSteps]();\n\n const storedError = stream._storedError;\n stream._writeRequests.forEach(writeRequest => {\n writeRequest._reject(storedError);\n });\n stream._writeRequests = new SimpleQueue();\n\n if (stream._pendingAbortRequest === undefined) {\n WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);\n return;\n }\n\n const abortRequest = stream._pendingAbortRequest;\n stream._pendingAbortRequest = undefined;\n\n if (abortRequest._wasAlreadyErroring) {\n abortRequest._reject(storedError);\n WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);\n return;\n }\n\n const promise = stream._writableStreamController[AbortSteps](abortRequest._reason);\n uponPromise(\n promise,\n () => {\n abortRequest._resolve();\n WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);\n },\n (reason: any) => {\n abortRequest._reject(reason);\n WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream);\n });\n}\n\nfunction WritableStreamFinishInFlightWrite(stream: WritableStream) {\n assert(stream._inFlightWriteRequest !== undefined);\n stream._inFlightWriteRequest!._resolve(undefined);\n stream._inFlightWriteRequest = undefined;\n}\n\nfunction WritableStreamFinishInFlightWriteWithError(stream: WritableStream, error: any) {\n assert(stream._inFlightWriteRequest !== undefined);\n stream._inFlightWriteRequest!._reject(error);\n stream._inFlightWriteRequest = undefined;\n\n assert(stream._state === 'writable' || stream._state === 'erroring');\n\n WritableStreamDealWithRejection(stream, error);\n}\n\nfunction WritableStreamFinishInFlightClose(stream: WritableStream) {\n assert(stream._inFlightCloseRequest !== undefined);\n stream._inFlightCloseRequest!._resolve(undefined);\n stream._inFlightCloseRequest = undefined;\n\n const state = stream._state;\n\n assert(state === 'writable' || state === 'erroring');\n\n if (state === 'erroring') {\n // The error was too late to do anything, so it is ignored.\n stream._storedError = undefined;\n if (stream._pendingAbortRequest !== undefined) {\n stream._pendingAbortRequest._resolve();\n stream._pendingAbortRequest = undefined;\n }\n }\n\n stream._state = 'closed';\n\n const writer = stream._writer;\n if (writer !== undefined) {\n defaultWriterClosedPromiseResolve(writer);\n }\n\n assert(stream._pendingAbortRequest === undefined);\n assert(stream._storedError === undefined);\n}\n\nfunction WritableStreamFinishInFlightCloseWithError(stream: WritableStream, error: any) {\n assert(stream._inFlightCloseRequest !== undefined);\n stream._inFlightCloseRequest!._reject(error);\n stream._inFlightCloseRequest = undefined;\n\n assert(stream._state === 'writable' || stream._state === 'erroring');\n\n // Never execute sink abort() after sink close().\n if (stream._pendingAbortRequest !== undefined) {\n stream._pendingAbortRequest._reject(error);\n stream._pendingAbortRequest = undefined;\n }\n WritableStreamDealWithRejection(stream, error);\n}\n\n// TODO(ricea): Fix alphabetical order.\nfunction WritableStreamCloseQueuedOrInFlight(stream: WritableStream): boolean {\n if (stream._closeRequest === undefined && stream._inFlightCloseRequest === undefined) {\n return false;\n }\n\n return true;\n}\n\nfunction WritableStreamHasOperationMarkedInFlight(stream: WritableStream): boolean {\n if (stream._inFlightWriteRequest === undefined && stream._inFlightCloseRequest === undefined) {\n return false;\n }\n\n return true;\n}\n\nfunction WritableStreamMarkCloseRequestInFlight(stream: WritableStream) {\n assert(stream._inFlightCloseRequest === undefined);\n assert(stream._closeRequest !== undefined);\n stream._inFlightCloseRequest = stream._closeRequest;\n stream._closeRequest = undefined;\n}\n\nfunction WritableStreamMarkFirstWriteRequestInFlight(stream: WritableStream) {\n assert(stream._inFlightWriteRequest === undefined);\n assert(stream._writeRequests.length !== 0);\n stream._inFlightWriteRequest = stream._writeRequests.shift();\n}\n\nfunction WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream: WritableStream) {\n assert(stream._state === 'errored');\n if (stream._closeRequest !== undefined) {\n assert(stream._inFlightCloseRequest === undefined);\n\n stream._closeRequest._reject(stream._storedError);\n stream._closeRequest = undefined;\n }\n const writer = stream._writer;\n if (writer !== undefined) {\n defaultWriterClosedPromiseReject(writer, stream._storedError);\n }\n}\n\nfunction WritableStreamUpdateBackpressure(stream: WritableStream, backpressure: boolean) {\n assert(stream._state === 'writable');\n assert(!WritableStreamCloseQueuedOrInFlight(stream));\n\n const writer = stream._writer;\n if (writer !== undefined && backpressure !== stream._backpressure) {\n if (backpressure) {\n defaultWriterReadyPromiseReset(writer);\n } else {\n assert(!backpressure);\n\n defaultWriterReadyPromiseResolve(writer);\n }\n }\n\n stream._backpressure = backpressure;\n}\n\n/**\n * A default writer vended by a {@link WritableStream}.\n *\n * @public\n */\nexport class WritableStreamDefaultWriter {\n /** @internal */\n _ownerWritableStream: WritableStream;\n /** @internal */\n _closedPromise!: Promise;\n /** @internal */\n _closedPromise_resolve?: (value?: undefined) => void;\n /** @internal */\n _closedPromise_reject?: (reason: any) => void;\n /** @internal */\n _closedPromiseState!: 'pending' | 'resolved' | 'rejected';\n /** @internal */\n _readyPromise!: Promise;\n /** @internal */\n _readyPromise_resolve?: (value?: undefined) => void;\n /** @internal */\n _readyPromise_reject?: (reason: any) => void;\n /** @internal */\n _readyPromiseState!: 'pending' | 'fulfilled' | 'rejected';\n\n constructor(stream: WritableStream) {\n assertRequiredArgument(stream, 1, 'WritableStreamDefaultWriter');\n assertWritableStream(stream, 'First parameter');\n\n if (IsWritableStreamLocked(stream)) {\n throw new TypeError('This stream has already been locked for exclusive writing by another writer');\n }\n\n this._ownerWritableStream = stream;\n stream._writer = this;\n\n const state = stream._state;\n\n if (state === 'writable') {\n if (!WritableStreamCloseQueuedOrInFlight(stream) && stream._backpressure) {\n defaultWriterReadyPromiseInitialize(this);\n } else {\n defaultWriterReadyPromiseInitializeAsResolved(this);\n }\n\n defaultWriterClosedPromiseInitialize(this);\n } else if (state === 'erroring') {\n defaultWriterReadyPromiseInitializeAsRejected(this, stream._storedError);\n defaultWriterClosedPromiseInitialize(this);\n } else if (state === 'closed') {\n defaultWriterReadyPromiseInitializeAsResolved(this);\n defaultWriterClosedPromiseInitializeAsResolved(this);\n } else {\n assert(state === 'errored');\n\n const storedError = stream._storedError;\n defaultWriterReadyPromiseInitializeAsRejected(this, storedError);\n defaultWriterClosedPromiseInitializeAsRejected(this, storedError);\n }\n }\n\n /**\n * Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the stream ever errors or\n * the writer’s lock is released before the stream finishes closing.\n */\n get closed(): Promise {\n if (!IsWritableStreamDefaultWriter(this)) {\n return promiseRejectedWith(defaultWriterBrandCheckException('closed'));\n }\n\n return this._closedPromise;\n }\n\n /**\n * Returns the desired size to fill the stream’s internal queue. It can be negative, if the queue is over-full.\n * A producer can use this information to determine the right amount of data to write.\n *\n * It will be `null` if the stream cannot be successfully written to (due to either being errored, or having an abort\n * queued up). It will return zero if the stream is closed. And the getter will throw an exception if invoked when\n * the writer’s lock is released.\n */\n get desiredSize(): number | null {\n if (!IsWritableStreamDefaultWriter(this)) {\n throw defaultWriterBrandCheckException('desiredSize');\n }\n\n if (this._ownerWritableStream === undefined) {\n throw defaultWriterLockException('desiredSize');\n }\n\n return WritableStreamDefaultWriterGetDesiredSize(this);\n }\n\n /**\n * Returns a promise that will be fulfilled when the desired size to fill the stream’s internal queue transitions\n * from non-positive to positive, signaling that it is no longer applying backpressure. Once the desired size dips\n * back to zero or below, the getter will return a new promise that stays pending until the next transition.\n *\n * If the stream becomes errored or aborted, or the writer’s lock is released, the returned promise will become\n * rejected.\n */\n get ready(): Promise {\n if (!IsWritableStreamDefaultWriter(this)) {\n return promiseRejectedWith(defaultWriterBrandCheckException('ready'));\n }\n\n return this._readyPromise;\n }\n\n /**\n * If the reader is active, behaves the same as {@link WritableStream.abort | stream.abort(reason)}.\n */\n abort(reason: any = undefined): Promise {\n if (!IsWritableStreamDefaultWriter(this)) {\n return promiseRejectedWith(defaultWriterBrandCheckException('abort'));\n }\n\n if (this._ownerWritableStream === undefined) {\n return promiseRejectedWith(defaultWriterLockException('abort'));\n }\n\n return WritableStreamDefaultWriterAbort(this, reason);\n }\n\n /**\n * If the reader is active, behaves the same as {@link WritableStream.close | stream.close()}.\n */\n close(): Promise {\n if (!IsWritableStreamDefaultWriter(this)) {\n return promiseRejectedWith(defaultWriterBrandCheckException('close'));\n }\n\n const stream = this._ownerWritableStream;\n\n if (stream === undefined) {\n return promiseRejectedWith(defaultWriterLockException('close'));\n }\n\n if (WritableStreamCloseQueuedOrInFlight(stream)) {\n return promiseRejectedWith(new TypeError('Cannot close an already-closing stream'));\n }\n\n return WritableStreamDefaultWriterClose(this);\n }\n\n /**\n * Releases the writer’s lock on the corresponding stream. After the lock is released, the writer is no longer active.\n * If the associated stream is errored when the lock is released, the writer will appear errored in the same way from\n * now on; otherwise, the writer will appear closed.\n *\n * Note that the lock can still be released even if some ongoing writes have not yet finished (i.e. even if the\n * promises returned from previous calls to {@link WritableStreamDefaultWriter.write | write()} have not yet settled).\n * It’s not necessary to hold the lock on the writer for the duration of the write; the lock instead simply prevents\n * other producers from writing in an interleaved manner.\n */\n releaseLock(): void {\n if (!IsWritableStreamDefaultWriter(this)) {\n throw defaultWriterBrandCheckException('releaseLock');\n }\n\n const stream = this._ownerWritableStream;\n\n if (stream === undefined) {\n return;\n }\n\n assert(stream._writer !== undefined);\n\n WritableStreamDefaultWriterRelease(this);\n }\n\n /**\n * Writes the given chunk to the writable stream, by waiting until any previous writes have finished successfully,\n * and then sending the chunk to the underlying sink's {@link UnderlyingSink.write | write()} method. It will return\n * a promise that fulfills with undefined upon a successful write, or rejects if the write fails or stream becomes\n * errored before the writing process is initiated.\n *\n * Note that what \"success\" means is up to the underlying sink; it might indicate simply that the chunk has been\n * accepted, and not necessarily that it is safely saved to its ultimate destination.\n */\n write(chunk: W): Promise;\n write(chunk: W = undefined!): Promise {\n if (!IsWritableStreamDefaultWriter(this)) {\n return promiseRejectedWith(defaultWriterBrandCheckException('write'));\n }\n\n if (this._ownerWritableStream === undefined) {\n return promiseRejectedWith(defaultWriterLockException('write to'));\n }\n\n return WritableStreamDefaultWriterWrite(this, chunk);\n }\n}\n\nObject.defineProperties(WritableStreamDefaultWriter.prototype, {\n abort: { enumerable: true },\n close: { enumerable: true },\n releaseLock: { enumerable: true },\n write: { enumerable: true },\n closed: { enumerable: true },\n desiredSize: { enumerable: true },\n ready: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(WritableStreamDefaultWriter.prototype, Symbol.toStringTag, {\n value: 'WritableStreamDefaultWriter',\n configurable: true\n });\n}\n\n// Abstract operations for the WritableStreamDefaultWriter.\n\nfunction IsWritableStreamDefaultWriter(x: any): x is WritableStreamDefaultWriter {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_ownerWritableStream')) {\n return false;\n }\n\n return true;\n}\n\n// A client of WritableStreamDefaultWriter may use these functions directly to bypass state check.\n\nfunction WritableStreamDefaultWriterAbort(writer: WritableStreamDefaultWriter, reason: any) {\n const stream = writer._ownerWritableStream;\n\n assert(stream !== undefined);\n\n return WritableStreamAbort(stream, reason);\n}\n\nfunction WritableStreamDefaultWriterClose(writer: WritableStreamDefaultWriter): Promise {\n const stream = writer._ownerWritableStream;\n\n assert(stream !== undefined);\n\n return WritableStreamClose(stream);\n}\n\nfunction WritableStreamDefaultWriterCloseWithErrorPropagation(writer: WritableStreamDefaultWriter): Promise {\n const stream = writer._ownerWritableStream;\n\n assert(stream !== undefined);\n\n const state = stream._state;\n if (WritableStreamCloseQueuedOrInFlight(stream) || state === 'closed') {\n return promiseResolvedWith(undefined);\n }\n\n if (state === 'errored') {\n return promiseRejectedWith(stream._storedError);\n }\n\n assert(state === 'writable' || state === 'erroring');\n\n return WritableStreamDefaultWriterClose(writer);\n}\n\nfunction WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer: WritableStreamDefaultWriter, error: any) {\n if (writer._closedPromiseState === 'pending') {\n defaultWriterClosedPromiseReject(writer, error);\n } else {\n defaultWriterClosedPromiseResetToRejected(writer, error);\n }\n}\n\nfunction WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer: WritableStreamDefaultWriter, error: any) {\n if (writer._readyPromiseState === 'pending') {\n defaultWriterReadyPromiseReject(writer, error);\n } else {\n defaultWriterReadyPromiseResetToRejected(writer, error);\n }\n}\n\nfunction WritableStreamDefaultWriterGetDesiredSize(writer: WritableStreamDefaultWriter): number | null {\n const stream = writer._ownerWritableStream;\n const state = stream._state;\n\n if (state === 'errored' || state === 'erroring') {\n return null;\n }\n\n if (state === 'closed') {\n return 0;\n }\n\n return WritableStreamDefaultControllerGetDesiredSize(stream._writableStreamController);\n}\n\nfunction WritableStreamDefaultWriterRelease(writer: WritableStreamDefaultWriter) {\n const stream = writer._ownerWritableStream;\n assert(stream !== undefined);\n assert(stream._writer === writer);\n\n const releasedError = new TypeError(\n `Writer was released and can no longer be used to monitor the stream's closedness`);\n\n WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError);\n\n // The state transitions to \"errored\" before the sink abort() method runs, but the writer.closed promise is not\n // rejected until afterwards. This means that simply testing state will not work.\n WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError);\n\n stream._writer = undefined;\n writer._ownerWritableStream = undefined!;\n}\n\nfunction WritableStreamDefaultWriterWrite(writer: WritableStreamDefaultWriter, chunk: W): Promise {\n const stream = writer._ownerWritableStream;\n\n assert(stream !== undefined);\n\n const controller = stream._writableStreamController;\n\n const chunkSize = WritableStreamDefaultControllerGetChunkSize(controller, chunk);\n\n if (stream !== writer._ownerWritableStream) {\n return promiseRejectedWith(defaultWriterLockException('write to'));\n }\n\n const state = stream._state;\n if (state === 'errored') {\n return promiseRejectedWith(stream._storedError);\n }\n if (WritableStreamCloseQueuedOrInFlight(stream) || state === 'closed') {\n return promiseRejectedWith(new TypeError('The stream is closing or closed and cannot be written to'));\n }\n if (state === 'erroring') {\n return promiseRejectedWith(stream._storedError);\n }\n\n assert(state === 'writable');\n\n const promise = WritableStreamAddWriteRequest(stream);\n\n WritableStreamDefaultControllerWrite(controller, chunk, chunkSize);\n\n return promise;\n}\n\nconst closeSentinel: unique symbol = {} as any;\n\ntype QueueRecord = W | typeof closeSentinel;\n\n/**\n * Allows control of a {@link WritableStream | writable stream}'s state and internal queue.\n *\n * @public\n */\nexport class WritableStreamDefaultController {\n /** @internal */\n _controlledWritableStream!: WritableStream;\n /** @internal */\n _queue!: SimpleQueue>>;\n /** @internal */\n _queueTotalSize!: number;\n /** @internal */\n _started!: boolean;\n /** @internal */\n _strategySizeAlgorithm!: QueuingStrategySizeCallback;\n /** @internal */\n _strategyHWM!: number;\n /** @internal */\n _writeAlgorithm!: (chunk: W) => Promise;\n /** @internal */\n _closeAlgorithm!: () => Promise;\n /** @internal */\n _abortAlgorithm!: (reason: any) => Promise;\n\n private constructor() {\n throw new TypeError('Illegal constructor');\n }\n\n /**\n * Closes the controlled writable stream, making all future interactions with it fail with the given error `e`.\n *\n * This method is rarely used, since usually it suffices to return a rejected promise from one of the underlying\n * sink's methods. However, it can be useful for suddenly shutting down a stream in response to an event outside the\n * normal lifecycle of interactions with the underlying sink.\n */\n error(e: any = undefined): void {\n if (!IsWritableStreamDefaultController(this)) {\n throw new TypeError(\n 'WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController');\n }\n const state = this._controlledWritableStream._state;\n if (state !== 'writable') {\n // The stream is closed, errored or will be soon. The sink can't do anything useful if it gets an error here, so\n // just treat it as a no-op.\n return;\n }\n\n WritableStreamDefaultControllerError(this, e);\n }\n\n /** @internal */\n [AbortSteps](reason: any) {\n const result = this._abortAlgorithm(reason);\n WritableStreamDefaultControllerClearAlgorithms(this);\n return result;\n }\n\n /** @internal */\n [ErrorSteps]() {\n ResetQueue(this);\n }\n}\n\nObject.defineProperties(WritableStreamDefaultController.prototype, {\n error: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(WritableStreamDefaultController.prototype, Symbol.toStringTag, {\n value: 'WritableStreamDefaultController',\n configurable: true\n });\n}\n\n// Abstract operations implementing interface required by the WritableStream.\n\nfunction IsWritableStreamDefaultController(x: any): x is WritableStreamDefaultController {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_controlledWritableStream')) {\n return false;\n }\n\n return true;\n}\n\nfunction SetUpWritableStreamDefaultController(stream: WritableStream,\n controller: WritableStreamDefaultController,\n startAlgorithm: () => void | PromiseLike,\n writeAlgorithm: (chunk: W) => Promise,\n closeAlgorithm: () => Promise,\n abortAlgorithm: (reason: any) => Promise,\n highWaterMark: number,\n sizeAlgorithm: QueuingStrategySizeCallback) {\n assert(IsWritableStream(stream));\n assert(stream._writableStreamController === undefined);\n\n controller._controlledWritableStream = stream;\n stream._writableStreamController = controller;\n\n // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly.\n controller._queue = undefined!;\n controller._queueTotalSize = undefined!;\n ResetQueue(controller);\n\n controller._started = false;\n\n controller._strategySizeAlgorithm = sizeAlgorithm;\n controller._strategyHWM = highWaterMark;\n\n controller._writeAlgorithm = writeAlgorithm;\n controller._closeAlgorithm = closeAlgorithm;\n controller._abortAlgorithm = abortAlgorithm;\n\n const backpressure = WritableStreamDefaultControllerGetBackpressure(controller);\n WritableStreamUpdateBackpressure(stream, backpressure);\n\n const startResult = startAlgorithm();\n const startPromise = promiseResolvedWith(startResult);\n uponPromise(\n startPromise,\n () => {\n assert(stream._state === 'writable' || stream._state === 'erroring');\n controller._started = true;\n WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);\n },\n r => {\n assert(stream._state === 'writable' || stream._state === 'erroring');\n controller._started = true;\n WritableStreamDealWithRejection(stream, r);\n }\n );\n}\n\nfunction SetUpWritableStreamDefaultControllerFromUnderlyingSink(stream: WritableStream,\n underlyingSink: ValidatedUnderlyingSink,\n highWaterMark: number,\n sizeAlgorithm: QueuingStrategySizeCallback) {\n const controller = Object.create(WritableStreamDefaultController.prototype);\n\n let startAlgorithm: () => void | PromiseLike = () => undefined;\n let writeAlgorithm: (chunk: W) => Promise = () => promiseResolvedWith(undefined);\n let closeAlgorithm: () => Promise = () => promiseResolvedWith(undefined);\n let abortAlgorithm: (reason: any) => Promise = () => promiseResolvedWith(undefined);\n\n if (underlyingSink.start !== undefined) {\n startAlgorithm = () => underlyingSink.start!(controller);\n }\n if (underlyingSink.write !== undefined) {\n writeAlgorithm = chunk => underlyingSink.write!(chunk, controller);\n }\n if (underlyingSink.close !== undefined) {\n closeAlgorithm = () => underlyingSink.close!();\n }\n if (underlyingSink.abort !== undefined) {\n abortAlgorithm = reason => underlyingSink.abort!(reason);\n }\n\n SetUpWritableStreamDefaultController(\n stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm\n );\n}\n\n// ClearAlgorithms may be called twice. Erroring the same stream in multiple ways will often result in redundant calls.\nfunction WritableStreamDefaultControllerClearAlgorithms(controller: WritableStreamDefaultController) {\n controller._writeAlgorithm = undefined!;\n controller._closeAlgorithm = undefined!;\n controller._abortAlgorithm = undefined!;\n controller._strategySizeAlgorithm = undefined!;\n}\n\nfunction WritableStreamDefaultControllerClose(controller: WritableStreamDefaultController) {\n EnqueueValueWithSize(controller, closeSentinel, 0);\n WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);\n}\n\nfunction WritableStreamDefaultControllerGetChunkSize(controller: WritableStreamDefaultController,\n chunk: W): number {\n try {\n return controller._strategySizeAlgorithm(chunk);\n } catch (chunkSizeE) {\n WritableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE);\n return 1;\n }\n}\n\nfunction WritableStreamDefaultControllerGetDesiredSize(controller: WritableStreamDefaultController): number {\n return controller._strategyHWM - controller._queueTotalSize;\n}\n\nfunction WritableStreamDefaultControllerWrite(controller: WritableStreamDefaultController,\n chunk: W,\n chunkSize: number) {\n try {\n EnqueueValueWithSize(controller, chunk, chunkSize);\n } catch (enqueueE) {\n WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueE);\n return;\n }\n\n const stream = controller._controlledWritableStream;\n if (!WritableStreamCloseQueuedOrInFlight(stream) && stream._state === 'writable') {\n const backpressure = WritableStreamDefaultControllerGetBackpressure(controller);\n WritableStreamUpdateBackpressure(stream, backpressure);\n }\n\n WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);\n}\n\n// Abstract operations for the WritableStreamDefaultController.\n\nfunction WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller: WritableStreamDefaultController) {\n const stream = controller._controlledWritableStream;\n\n if (!controller._started) {\n return;\n }\n\n if (stream._inFlightWriteRequest !== undefined) {\n return;\n }\n\n const state = stream._state;\n assert(state !== 'closed' && state !== 'errored');\n if (state === 'erroring') {\n WritableStreamFinishErroring(stream);\n return;\n }\n\n if (controller._queue.length === 0) {\n return;\n }\n\n const value = PeekQueueValue(controller);\n if (value === closeSentinel) {\n WritableStreamDefaultControllerProcessClose(controller);\n } else {\n WritableStreamDefaultControllerProcessWrite(controller, value);\n }\n}\n\nfunction WritableStreamDefaultControllerErrorIfNeeded(controller: WritableStreamDefaultController, error: any) {\n if (controller._controlledWritableStream._state === 'writable') {\n WritableStreamDefaultControllerError(controller, error);\n }\n}\n\nfunction WritableStreamDefaultControllerProcessClose(controller: WritableStreamDefaultController) {\n const stream = controller._controlledWritableStream;\n\n WritableStreamMarkCloseRequestInFlight(stream);\n\n DequeueValue(controller);\n assert(controller._queue.length === 0);\n\n const sinkClosePromise = controller._closeAlgorithm();\n WritableStreamDefaultControllerClearAlgorithms(controller);\n uponPromise(\n sinkClosePromise,\n () => {\n WritableStreamFinishInFlightClose(stream);\n },\n reason => {\n WritableStreamFinishInFlightCloseWithError(stream, reason);\n }\n );\n}\n\nfunction WritableStreamDefaultControllerProcessWrite(controller: WritableStreamDefaultController, chunk: W) {\n const stream = controller._controlledWritableStream;\n\n WritableStreamMarkFirstWriteRequestInFlight(stream);\n\n const sinkWritePromise = controller._writeAlgorithm(chunk);\n uponPromise(\n sinkWritePromise,\n () => {\n WritableStreamFinishInFlightWrite(stream);\n\n const state = stream._state;\n assert(state === 'writable' || state === 'erroring');\n\n DequeueValue(controller);\n\n if (!WritableStreamCloseQueuedOrInFlight(stream) && state === 'writable') {\n const backpressure = WritableStreamDefaultControllerGetBackpressure(controller);\n WritableStreamUpdateBackpressure(stream, backpressure);\n }\n\n WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller);\n },\n reason => {\n if (stream._state === 'writable') {\n WritableStreamDefaultControllerClearAlgorithms(controller);\n }\n WritableStreamFinishInFlightWriteWithError(stream, reason);\n }\n );\n}\n\nfunction WritableStreamDefaultControllerGetBackpressure(controller: WritableStreamDefaultController): boolean {\n const desiredSize = WritableStreamDefaultControllerGetDesiredSize(controller);\n return desiredSize <= 0;\n}\n\n// A client of WritableStreamDefaultController may use these functions directly to bypass state check.\n\nfunction WritableStreamDefaultControllerError(controller: WritableStreamDefaultController, error: any) {\n const stream = controller._controlledWritableStream;\n\n assert(stream._state === 'writable');\n\n WritableStreamDefaultControllerClearAlgorithms(controller);\n WritableStreamStartErroring(stream, error);\n}\n\n// Helper functions for the WritableStream.\n\nfunction streamBrandCheckException(name: string): TypeError {\n return new TypeError(`WritableStream.prototype.${name} can only be used on a WritableStream`);\n}\n\n// Helper functions for the WritableStreamDefaultWriter.\n\nfunction defaultWriterBrandCheckException(name: string): TypeError {\n return new TypeError(\n `WritableStreamDefaultWriter.prototype.${name} can only be used on a WritableStreamDefaultWriter`);\n}\n\nfunction defaultWriterLockException(name: string): TypeError {\n return new TypeError('Cannot ' + name + ' a stream using a released writer');\n}\n\nfunction defaultWriterClosedPromiseInitialize(writer: WritableStreamDefaultWriter) {\n writer._closedPromise = newPromise((resolve, reject) => {\n writer._closedPromise_resolve = resolve;\n writer._closedPromise_reject = reject;\n writer._closedPromiseState = 'pending';\n });\n}\n\nfunction defaultWriterClosedPromiseInitializeAsRejected(writer: WritableStreamDefaultWriter, reason: any) {\n defaultWriterClosedPromiseInitialize(writer);\n defaultWriterClosedPromiseReject(writer, reason);\n}\n\nfunction defaultWriterClosedPromiseInitializeAsResolved(writer: WritableStreamDefaultWriter) {\n defaultWriterClosedPromiseInitialize(writer);\n defaultWriterClosedPromiseResolve(writer);\n}\n\nfunction defaultWriterClosedPromiseReject(writer: WritableStreamDefaultWriter, reason: any) {\n if (writer._closedPromise_reject === undefined) {\n return;\n }\n assert(writer._closedPromiseState === 'pending');\n\n setPromiseIsHandledToTrue(writer._closedPromise);\n writer._closedPromise_reject(reason);\n writer._closedPromise_resolve = undefined;\n writer._closedPromise_reject = undefined;\n writer._closedPromiseState = 'rejected';\n}\n\nfunction defaultWriterClosedPromiseResetToRejected(writer: WritableStreamDefaultWriter, reason: any) {\n assert(writer._closedPromise_resolve === undefined);\n assert(writer._closedPromise_reject === undefined);\n assert(writer._closedPromiseState !== 'pending');\n\n defaultWriterClosedPromiseInitializeAsRejected(writer, reason);\n}\n\nfunction defaultWriterClosedPromiseResolve(writer: WritableStreamDefaultWriter) {\n if (writer._closedPromise_resolve === undefined) {\n return;\n }\n assert(writer._closedPromiseState === 'pending');\n\n writer._closedPromise_resolve(undefined);\n writer._closedPromise_resolve = undefined;\n writer._closedPromise_reject = undefined;\n writer._closedPromiseState = 'resolved';\n}\n\nfunction defaultWriterReadyPromiseInitialize(writer: WritableStreamDefaultWriter) {\n writer._readyPromise = newPromise((resolve, reject) => {\n writer._readyPromise_resolve = resolve;\n writer._readyPromise_reject = reject;\n });\n writer._readyPromiseState = 'pending';\n}\n\nfunction defaultWriterReadyPromiseInitializeAsRejected(writer: WritableStreamDefaultWriter, reason: any) {\n defaultWriterReadyPromiseInitialize(writer);\n defaultWriterReadyPromiseReject(writer, reason);\n}\n\nfunction defaultWriterReadyPromiseInitializeAsResolved(writer: WritableStreamDefaultWriter) {\n defaultWriterReadyPromiseInitialize(writer);\n defaultWriterReadyPromiseResolve(writer);\n}\n\nfunction defaultWriterReadyPromiseReject(writer: WritableStreamDefaultWriter, reason: any) {\n if (writer._readyPromise_reject === undefined) {\n return;\n }\n\n setPromiseIsHandledToTrue(writer._readyPromise);\n writer._readyPromise_reject(reason);\n writer._readyPromise_resolve = undefined;\n writer._readyPromise_reject = undefined;\n writer._readyPromiseState = 'rejected';\n}\n\nfunction defaultWriterReadyPromiseReset(writer: WritableStreamDefaultWriter) {\n assert(writer._readyPromise_resolve === undefined);\n assert(writer._readyPromise_reject === undefined);\n\n defaultWriterReadyPromiseInitialize(writer);\n}\n\nfunction defaultWriterReadyPromiseResetToRejected(writer: WritableStreamDefaultWriter, reason: any) {\n assert(writer._readyPromise_resolve === undefined);\n assert(writer._readyPromise_reject === undefined);\n\n defaultWriterReadyPromiseInitializeAsRejected(writer, reason);\n}\n\nfunction defaultWriterReadyPromiseResolve(writer: WritableStreamDefaultWriter) {\n if (writer._readyPromise_resolve === undefined) {\n return;\n }\n\n writer._readyPromise_resolve(undefined);\n writer._readyPromise_resolve = undefined;\n writer._readyPromise_reject = undefined;\n writer._readyPromiseState = 'fulfilled';\n}\n","/// \nexport const NativeDOMException: typeof DOMException | undefined = typeof DOMException !== 'undefined' ? DOMException : undefined;\n","/// \nimport { NativeDOMException } from './native';\n\ndeclare class DOMExceptionClass extends Error {\n constructor(message?: string, name?: string);\n\n name: string;\n message: string;\n}\n\ntype DOMException = DOMExceptionClass;\ntype DOMExceptionConstructor = typeof DOMExceptionClass;\n\nfunction isDOMExceptionConstructor(ctor: unknown): ctor is DOMExceptionConstructor {\n if (!(typeof ctor === 'function' || typeof ctor === 'object')) {\n return false;\n }\n try {\n new (ctor as DOMExceptionConstructor)();\n return true;\n } catch {\n return false;\n }\n}\n\nfunction createDOMExceptionPolyfill(): DOMExceptionConstructor {\n const ctor = function DOMException(this: DOMException, message?: string, name?: string) {\n this.message = message || '';\n this.name = name || 'Error';\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n } as any;\n ctor.prototype = Object.create(Error.prototype);\n Object.defineProperty(ctor.prototype, 'constructor', { value: ctor, writable: true, configurable: true });\n return ctor;\n}\n\nconst DOMException: DOMExceptionConstructor =\n isDOMExceptionConstructor(NativeDOMException) ? NativeDOMException : createDOMExceptionPolyfill();\n\nexport { DOMException };\n","import { IsReadableStream, IsReadableStreamLocked, ReadableStream, ReadableStreamCancel } from '../readable-stream';\nimport { AcquireReadableStreamDefaultReader, ReadableStreamDefaultReaderRead } from './default-reader';\nimport { ReadableStreamReaderGenericRelease } from './generic-reader';\nimport {\n AcquireWritableStreamDefaultWriter,\n IsWritableStream,\n IsWritableStreamLocked,\n WritableStream,\n WritableStreamAbort,\n WritableStreamCloseQueuedOrInFlight,\n WritableStreamDefaultWriterCloseWithErrorPropagation,\n WritableStreamDefaultWriterRelease,\n WritableStreamDefaultWriterWrite\n} from '../writable-stream';\nimport assert from '../../stub/assert';\nimport {\n newPromise,\n PerformPromiseThen,\n promiseResolvedWith,\n setPromiseIsHandledToTrue,\n uponFulfillment,\n uponPromise,\n uponRejection\n} from '../helpers/webidl';\nimport { noop } from '../../utils';\nimport { AbortSignal, isAbortSignal } from '../abort-signal';\nimport { DOMException } from '../../stub/dom-exception';\n\nexport function ReadableStreamPipeTo(source: ReadableStream,\n dest: WritableStream,\n preventClose: boolean,\n preventAbort: boolean,\n preventCancel: boolean,\n signal: AbortSignal | undefined): Promise {\n assert(IsReadableStream(source));\n assert(IsWritableStream(dest));\n assert(typeof preventClose === 'boolean');\n assert(typeof preventAbort === 'boolean');\n assert(typeof preventCancel === 'boolean');\n assert(signal === undefined || isAbortSignal(signal));\n assert(!IsReadableStreamLocked(source));\n assert(!IsWritableStreamLocked(dest));\n\n const reader = AcquireReadableStreamDefaultReader(source);\n const writer = AcquireWritableStreamDefaultWriter(dest);\n\n source._disturbed = true;\n\n let shuttingDown = false;\n\n // This is used to keep track of the spec's requirement that we wait for ongoing writes during shutdown.\n let currentWrite = promiseResolvedWith(undefined);\n\n return newPromise((resolve, reject) => {\n let abortAlgorithm: () => void;\n if (signal !== undefined) {\n abortAlgorithm = () => {\n const error = new DOMException('Aborted', 'AbortError');\n const actions: Array<() => Promise> = [];\n if (!preventAbort) {\n actions.push(() => {\n if (dest._state === 'writable') {\n return WritableStreamAbort(dest, error);\n }\n return promiseResolvedWith(undefined);\n });\n }\n if (!preventCancel) {\n actions.push(() => {\n if (source._state === 'readable') {\n return ReadableStreamCancel(source, error);\n }\n return promiseResolvedWith(undefined);\n });\n }\n shutdownWithAction(() => Promise.all(actions.map(action => action())), true, error);\n };\n\n if (signal.aborted) {\n abortAlgorithm();\n return;\n }\n\n signal.addEventListener('abort', abortAlgorithm);\n }\n\n // Using reader and writer, read all chunks from this and write them to dest\n // - Backpressure must be enforced\n // - Shutdown must stop all activity\n function pipeLoop() {\n return newPromise((resolveLoop, rejectLoop) => {\n function next(done: boolean) {\n if (done) {\n resolveLoop();\n } else {\n // Use `PerformPromiseThen` instead of `uponPromise` to avoid\n // adding unnecessary `.catch(rethrowAssertionErrorRejection)` handlers\n PerformPromiseThen(pipeStep(), next, rejectLoop);\n }\n }\n\n next(false);\n });\n }\n\n function pipeStep(): Promise {\n if (shuttingDown) {\n return promiseResolvedWith(true);\n }\n\n return PerformPromiseThen(writer._readyPromise, () => {\n return newPromise((resolveRead, rejectRead) => {\n ReadableStreamDefaultReaderRead(\n reader,\n {\n _chunkSteps: chunk => {\n currentWrite = PerformPromiseThen(WritableStreamDefaultWriterWrite(writer, chunk), undefined, noop);\n resolveRead(false);\n },\n _closeSteps: () => resolveRead(true),\n _errorSteps: rejectRead\n }\n );\n });\n });\n }\n\n // Errors must be propagated forward\n isOrBecomesErrored(source, reader._closedPromise, storedError => {\n if (!preventAbort) {\n shutdownWithAction(() => WritableStreamAbort(dest, storedError), true, storedError);\n } else {\n shutdown(true, storedError);\n }\n });\n\n // Errors must be propagated backward\n isOrBecomesErrored(dest, writer._closedPromise, storedError => {\n if (!preventCancel) {\n shutdownWithAction(() => ReadableStreamCancel(source, storedError), true, storedError);\n } else {\n shutdown(true, storedError);\n }\n });\n\n // Closing must be propagated forward\n isOrBecomesClosed(source, reader._closedPromise, () => {\n if (!preventClose) {\n shutdownWithAction(() => WritableStreamDefaultWriterCloseWithErrorPropagation(writer));\n } else {\n shutdown();\n }\n });\n\n // Closing must be propagated backward\n if (WritableStreamCloseQueuedOrInFlight(dest) || dest._state === 'closed') {\n const destClosed = new TypeError('the destination writable stream closed before all data could be piped to it');\n\n if (!preventCancel) {\n shutdownWithAction(() => ReadableStreamCancel(source, destClosed), true, destClosed);\n } else {\n shutdown(true, destClosed);\n }\n }\n\n setPromiseIsHandledToTrue(pipeLoop());\n\n function waitForWritesToFinish(): Promise {\n // Another write may have started while we were waiting on this currentWrite, so we have to be sure to wait\n // for that too.\n const oldCurrentWrite = currentWrite;\n return PerformPromiseThen(\n currentWrite,\n () => oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : undefined\n );\n }\n\n function isOrBecomesErrored(stream: ReadableStream | WritableStream,\n promise: Promise,\n action: (reason: any) => void) {\n if (stream._state === 'errored') {\n action(stream._storedError);\n } else {\n uponRejection(promise, action);\n }\n }\n\n function isOrBecomesClosed(stream: ReadableStream | WritableStream, promise: Promise, action: () => void) {\n if (stream._state === 'closed') {\n action();\n } else {\n uponFulfillment(promise, action);\n }\n }\n\n function shutdownWithAction(action: () => Promise, originalIsError?: boolean, originalError?: any) {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n\n if (dest._state === 'writable' && !WritableStreamCloseQueuedOrInFlight(dest)) {\n uponFulfillment(waitForWritesToFinish(), doTheRest);\n } else {\n doTheRest();\n }\n\n function doTheRest() {\n uponPromise(\n action(),\n () => finalize(originalIsError, originalError),\n newError => finalize(true, newError)\n );\n }\n }\n\n function shutdown(isError?: boolean, error?: any) {\n if (shuttingDown) {\n return;\n }\n shuttingDown = true;\n\n if (dest._state === 'writable' && !WritableStreamCloseQueuedOrInFlight(dest)) {\n uponFulfillment(waitForWritesToFinish(), () => finalize(isError, error));\n } else {\n finalize(isError, error);\n }\n }\n\n function finalize(isError?: boolean, error?: any) {\n WritableStreamDefaultWriterRelease(writer);\n ReadableStreamReaderGenericRelease(reader);\n\n if (signal !== undefined) {\n signal.removeEventListener('abort', abortAlgorithm);\n }\n if (isError) {\n reject(error);\n } else {\n resolve(undefined);\n }\n }\n });\n}\n","import { QueuingStrategySizeCallback } from '../queuing-strategy';\nimport assert from '../../stub/assert';\nimport { DequeueValue, EnqueueValueWithSize, QueuePair, ResetQueue } from '../abstract-ops/queue-with-sizes';\nimport {\n ReadableStreamAddReadRequest,\n ReadableStreamFulfillReadRequest,\n ReadableStreamGetNumReadRequests,\n ReadRequest\n} from './default-reader';\nimport { SimpleQueue } from '../simple-queue';\nimport { IsReadableStreamLocked, ReadableStream, ReadableStreamClose, ReadableStreamError } from '../readable-stream';\nimport { ValidatedUnderlyingSource } from './underlying-source';\nimport { typeIsObject } from '../helpers/miscellaneous';\nimport { CancelSteps, PullSteps } from '../abstract-ops/internal-methods';\nimport { promiseResolvedWith, uponPromise } from '../helpers/webidl';\n\n/**\n * Allows control of a {@link ReadableStream | readable stream}'s state and internal queue.\n *\n * @public\n */\nexport class ReadableStreamDefaultController {\n /** @internal */\n _controlledReadableStream!: ReadableStream;\n /** @internal */\n _queue!: SimpleQueue>;\n /** @internal */\n _queueTotalSize!: number;\n /** @internal */\n _started!: boolean;\n /** @internal */\n _closeRequested!: boolean;\n /** @internal */\n _pullAgain!: boolean;\n /** @internal */\n _pulling !: boolean;\n /** @internal */\n _strategySizeAlgorithm!: QueuingStrategySizeCallback;\n /** @internal */\n _strategyHWM!: number;\n /** @internal */\n _pullAlgorithm!: () => Promise;\n /** @internal */\n _cancelAlgorithm!: (reason: any) => Promise;\n\n private constructor() {\n throw new TypeError('Illegal constructor');\n }\n\n /**\n * Returns the desired size to fill the controlled stream's internal queue. It can be negative, if the queue is\n * over-full. An underlying source ought to use this information to determine when and how to apply backpressure.\n */\n get desiredSize(): number | null {\n if (!IsReadableStreamDefaultController(this)) {\n throw defaultControllerBrandCheckException('desiredSize');\n }\n\n return ReadableStreamDefaultControllerGetDesiredSize(this);\n }\n\n /**\n * Closes the controlled readable stream. Consumers will still be able to read any previously-enqueued chunks from\n * the stream, but once those are read, the stream will become closed.\n */\n close(): void {\n if (!IsReadableStreamDefaultController(this)) {\n throw defaultControllerBrandCheckException('close');\n }\n\n if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(this)) {\n throw new TypeError('The stream is not in a state that permits close');\n }\n\n ReadableStreamDefaultControllerClose(this);\n }\n\n /**\n * Enqueues the given chunk `chunk` in the controlled readable stream.\n */\n enqueue(chunk: R): void;\n enqueue(chunk: R = undefined!): void {\n if (!IsReadableStreamDefaultController(this)) {\n throw defaultControllerBrandCheckException('enqueue');\n }\n\n if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(this)) {\n throw new TypeError('The stream is not in a state that permits enqueue');\n }\n\n return ReadableStreamDefaultControllerEnqueue(this, chunk);\n }\n\n /**\n * Errors the controlled readable stream, making all future interactions with it fail with the given error `e`.\n */\n error(e: any = undefined): void {\n if (!IsReadableStreamDefaultController(this)) {\n throw defaultControllerBrandCheckException('error');\n }\n\n ReadableStreamDefaultControllerError(this, e);\n }\n\n /** @internal */\n [CancelSteps](reason: any): Promise {\n ResetQueue(this);\n const result = this._cancelAlgorithm(reason);\n ReadableStreamDefaultControllerClearAlgorithms(this);\n return result;\n }\n\n /** @internal */\n [PullSteps](readRequest: ReadRequest): void {\n const stream = this._controlledReadableStream;\n\n if (this._queue.length > 0) {\n const chunk = DequeueValue(this);\n\n if (this._closeRequested && this._queue.length === 0) {\n ReadableStreamDefaultControllerClearAlgorithms(this);\n ReadableStreamClose(stream);\n } else {\n ReadableStreamDefaultControllerCallPullIfNeeded(this);\n }\n\n readRequest._chunkSteps(chunk);\n } else {\n ReadableStreamAddReadRequest(stream, readRequest);\n ReadableStreamDefaultControllerCallPullIfNeeded(this);\n }\n }\n}\n\nObject.defineProperties(ReadableStreamDefaultController.prototype, {\n close: { enumerable: true },\n enqueue: { enumerable: true },\n error: { enumerable: true },\n desiredSize: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(ReadableStreamDefaultController.prototype, Symbol.toStringTag, {\n value: 'ReadableStreamDefaultController',\n configurable: true\n });\n}\n\n// Abstract operations for the ReadableStreamDefaultController.\n\nfunction IsReadableStreamDefaultController(x: any): x is ReadableStreamDefaultController {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_controlledReadableStream')) {\n return false;\n }\n\n return true;\n}\n\nfunction ReadableStreamDefaultControllerCallPullIfNeeded(controller: ReadableStreamDefaultController): void {\n const shouldPull = ReadableStreamDefaultControllerShouldCallPull(controller);\n if (!shouldPull) {\n return;\n }\n\n if (controller._pulling) {\n controller._pullAgain = true;\n return;\n }\n\n assert(!controller._pullAgain);\n\n controller._pulling = true;\n\n const pullPromise = controller._pullAlgorithm();\n uponPromise(\n pullPromise,\n () => {\n controller._pulling = false;\n\n if (controller._pullAgain) {\n controller._pullAgain = false;\n ReadableStreamDefaultControllerCallPullIfNeeded(controller);\n }\n },\n e => {\n ReadableStreamDefaultControllerError(controller, e);\n }\n );\n}\n\nfunction ReadableStreamDefaultControllerShouldCallPull(controller: ReadableStreamDefaultController): boolean {\n const stream = controller._controlledReadableStream;\n\n if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) {\n return false;\n }\n\n if (!controller._started) {\n return false;\n }\n\n if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) {\n return true;\n }\n\n const desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller);\n assert(desiredSize !== null);\n if (desiredSize! > 0) {\n return true;\n }\n\n return false;\n}\n\nfunction ReadableStreamDefaultControllerClearAlgorithms(controller: ReadableStreamDefaultController) {\n controller._pullAlgorithm = undefined!;\n controller._cancelAlgorithm = undefined!;\n controller._strategySizeAlgorithm = undefined!;\n}\n\n// A client of ReadableStreamDefaultController may use these functions directly to bypass state check.\n\nexport function ReadableStreamDefaultControllerClose(controller: ReadableStreamDefaultController) {\n if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) {\n return;\n }\n\n const stream = controller._controlledReadableStream;\n\n controller._closeRequested = true;\n\n if (controller._queue.length === 0) {\n ReadableStreamDefaultControllerClearAlgorithms(controller);\n ReadableStreamClose(stream);\n }\n}\n\nexport function ReadableStreamDefaultControllerEnqueue(controller: ReadableStreamDefaultController, chunk: R): void {\n if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) {\n return;\n }\n\n const stream = controller._controlledReadableStream;\n\n if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) {\n ReadableStreamFulfillReadRequest(stream, chunk, false);\n } else {\n let chunkSize;\n try {\n chunkSize = controller._strategySizeAlgorithm(chunk);\n } catch (chunkSizeE) {\n ReadableStreamDefaultControllerError(controller, chunkSizeE);\n throw chunkSizeE;\n }\n\n try {\n EnqueueValueWithSize(controller, chunk, chunkSize);\n } catch (enqueueE) {\n ReadableStreamDefaultControllerError(controller, enqueueE);\n throw enqueueE;\n }\n }\n\n ReadableStreamDefaultControllerCallPullIfNeeded(controller);\n}\n\nexport function ReadableStreamDefaultControllerError(controller: ReadableStreamDefaultController, e: any) {\n const stream = controller._controlledReadableStream;\n\n if (stream._state !== 'readable') {\n return;\n }\n\n ResetQueue(controller);\n\n ReadableStreamDefaultControllerClearAlgorithms(controller);\n ReadableStreamError(stream, e);\n}\n\nexport function ReadableStreamDefaultControllerGetDesiredSize(controller: ReadableStreamDefaultController): number | null {\n const state = controller._controlledReadableStream._state;\n\n if (state === 'errored') {\n return null;\n }\n if (state === 'closed') {\n return 0;\n }\n\n return controller._strategyHWM - controller._queueTotalSize;\n}\n\n// This is used in the implementation of TransformStream.\nexport function ReadableStreamDefaultControllerHasBackpressure(controller: ReadableStreamDefaultController): boolean {\n if (ReadableStreamDefaultControllerShouldCallPull(controller)) {\n return false;\n }\n\n return true;\n}\n\nexport function ReadableStreamDefaultControllerCanCloseOrEnqueue(controller: ReadableStreamDefaultController): boolean {\n const state = controller._controlledReadableStream._state;\n\n if (!controller._closeRequested && state === 'readable') {\n return true;\n }\n\n return false;\n}\n\nexport function SetUpReadableStreamDefaultController(stream: ReadableStream,\n controller: ReadableStreamDefaultController,\n startAlgorithm: () => void | PromiseLike,\n pullAlgorithm: () => Promise,\n cancelAlgorithm: (reason: any) => Promise,\n highWaterMark: number,\n sizeAlgorithm: QueuingStrategySizeCallback) {\n assert(stream._readableStreamController === undefined);\n\n controller._controlledReadableStream = stream;\n\n controller._queue = undefined!;\n controller._queueTotalSize = undefined!;\n ResetQueue(controller);\n\n controller._started = false;\n controller._closeRequested = false;\n controller._pullAgain = false;\n controller._pulling = false;\n\n controller._strategySizeAlgorithm = sizeAlgorithm;\n controller._strategyHWM = highWaterMark;\n\n controller._pullAlgorithm = pullAlgorithm;\n controller._cancelAlgorithm = cancelAlgorithm;\n\n stream._readableStreamController = controller;\n\n const startResult = startAlgorithm();\n uponPromise(\n promiseResolvedWith(startResult),\n () => {\n controller._started = true;\n\n assert(!controller._pulling);\n assert(!controller._pullAgain);\n\n ReadableStreamDefaultControllerCallPullIfNeeded(controller);\n },\n r => {\n ReadableStreamDefaultControllerError(controller, r);\n }\n );\n}\n\nexport function SetUpReadableStreamDefaultControllerFromUnderlyingSource(\n stream: ReadableStream,\n underlyingSource: ValidatedUnderlyingSource,\n highWaterMark: number,\n sizeAlgorithm: QueuingStrategySizeCallback\n) {\n const controller: ReadableStreamDefaultController = Object.create(ReadableStreamDefaultController.prototype);\n\n let startAlgorithm: () => void | PromiseLike = () => undefined;\n let pullAlgorithm: () => Promise = () => promiseResolvedWith(undefined);\n let cancelAlgorithm: (reason: any) => Promise = () => promiseResolvedWith(undefined);\n\n if (underlyingSource.start !== undefined) {\n startAlgorithm = () => underlyingSource.start!(controller);\n }\n if (underlyingSource.pull !== undefined) {\n pullAlgorithm = () => underlyingSource.pull!(controller);\n }\n if (underlyingSource.cancel !== undefined) {\n cancelAlgorithm = reason => underlyingSource.cancel!(reason);\n }\n\n SetUpReadableStreamDefaultController(\n stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm\n );\n}\n\n// Helper functions for the ReadableStreamDefaultController.\n\nfunction defaultControllerBrandCheckException(name: string): TypeError {\n return new TypeError(\n `ReadableStreamDefaultController.prototype.${name} can only be used on a ReadableStreamDefaultController`);\n}\n","import { assertDictionary, assertFunction, convertUnsignedLongLongWithEnforceRange } from './basic';\nimport {\n ReadableStreamController,\n UnderlyingByteSource,\n UnderlyingDefaultOrByteSource,\n UnderlyingDefaultOrByteSourcePullCallback,\n UnderlyingDefaultOrByteSourceStartCallback,\n UnderlyingSource,\n UnderlyingSourceCancelCallback,\n ValidatedUnderlyingDefaultOrByteSource\n} from '../readable-stream/underlying-source';\nimport { promiseCall, reflectCall } from '../helpers/webidl';\n\nexport function convertUnderlyingDefaultOrByteSource(\n source: UnderlyingSource | UnderlyingByteSource | null,\n context: string\n): ValidatedUnderlyingDefaultOrByteSource {\n assertDictionary(source, context);\n const original = source as (UnderlyingDefaultOrByteSource | null);\n const autoAllocateChunkSize = original?.autoAllocateChunkSize;\n const cancel = original?.cancel;\n const pull = original?.pull;\n const start = original?.start;\n const type = original?.type;\n return {\n autoAllocateChunkSize: autoAllocateChunkSize === undefined ?\n undefined :\n convertUnsignedLongLongWithEnforceRange(\n autoAllocateChunkSize,\n `${context} has member 'autoAllocateChunkSize' that`\n ),\n cancel: cancel === undefined ?\n undefined :\n convertUnderlyingSourceCancelCallback(cancel, original!, `${context} has member 'cancel' that`),\n pull: pull === undefined ?\n undefined :\n convertUnderlyingSourcePullCallback(pull, original!, `${context} has member 'pull' that`),\n start: start === undefined ?\n undefined :\n convertUnderlyingSourceStartCallback(start, original!, `${context} has member 'start' that`),\n type: type === undefined ? undefined : convertReadableStreamType(type, `${context} has member 'type' that`)\n };\n}\n\nfunction convertUnderlyingSourceCancelCallback(\n fn: UnderlyingSourceCancelCallback,\n original: UnderlyingDefaultOrByteSource,\n context: string\n): (reason: any) => Promise {\n assertFunction(fn, context);\n return (reason: any) => promiseCall(fn, original, [reason]);\n}\n\nfunction convertUnderlyingSourcePullCallback(\n fn: UnderlyingDefaultOrByteSourcePullCallback,\n original: UnderlyingDefaultOrByteSource,\n context: string\n): (controller: ReadableStreamController) => Promise {\n assertFunction(fn, context);\n return (controller: ReadableStreamController) => promiseCall(fn, original, [controller]);\n}\n\nfunction convertUnderlyingSourceStartCallback(\n fn: UnderlyingDefaultOrByteSourceStartCallback,\n original: UnderlyingDefaultOrByteSource,\n context: string\n): UnderlyingDefaultOrByteSourceStartCallback {\n assertFunction(fn, context);\n return (controller: ReadableStreamController) => reflectCall(fn, original, [controller]);\n}\n\nfunction convertReadableStreamType(type: string, context: string): 'bytes' {\n type = `${type}`;\n if (type !== 'bytes') {\n throw new TypeError(`${context} '${type}' is not a valid enumeration value for ReadableStreamType`);\n }\n return type;\n}\n","import { assertDictionary } from './basic';\nimport { ReadableStreamGetReaderOptions } from '../readable-stream/reader-options';\n\nexport function convertReaderOptions(options: ReadableStreamGetReaderOptions | null | undefined,\n context: string): ReadableStreamGetReaderOptions {\n assertDictionary(options, context);\n const mode = options?.mode;\n return {\n mode: mode === undefined ? undefined : convertReadableStreamReaderMode(mode, `${context} has member 'mode' that`)\n };\n}\n\nfunction convertReadableStreamReaderMode(mode: string, context: string): 'byob' {\n mode = `${mode}`;\n if (mode !== 'byob') {\n throw new TypeError(`${context} '${mode}' is not a valid enumeration value for ReadableStreamReaderMode`);\n }\n return mode;\n}\n","import { assertDictionary } from './basic';\nimport { StreamPipeOptions, ValidatedStreamPipeOptions } from '../readable-stream/pipe-options';\nimport { AbortSignal, isAbortSignal } from '../abort-signal';\n\nexport function convertPipeOptions(options: StreamPipeOptions | null | undefined,\n context: string): ValidatedStreamPipeOptions {\n assertDictionary(options, context);\n const preventAbort = options?.preventAbort;\n const preventCancel = options?.preventCancel;\n const preventClose = options?.preventClose;\n const signal = options?.signal;\n if (signal !== undefined) {\n assertAbortSignal(signal, `${context} has member 'signal' that`);\n }\n return {\n preventAbort: Boolean(preventAbort),\n preventCancel: Boolean(preventCancel),\n preventClose: Boolean(preventClose),\n signal\n };\n}\n\nfunction assertAbortSignal(signal: unknown, context: string): asserts signal is AbortSignal {\n if (!isAbortSignal(signal)) {\n throw new TypeError(`${context} is not an AbortSignal.`);\n }\n}\n","/**\n * A signal object that allows you to communicate with a request and abort it if required\n * via its associated `AbortController` object.\n *\n * @remarks\n * This interface is compatible with the `AbortSignal` interface defined in TypeScript's DOM types.\n * It is redefined here, so it can be polyfilled without a DOM, for example with\n * {@link https://www.npmjs.com/package/abortcontroller-polyfill | abortcontroller-polyfill} in a Node environment.\n *\n * @public\n */\nexport interface AbortSignal {\n /**\n * Whether the request is aborted.\n */\n readonly aborted: boolean;\n\n /**\n * Add an event listener to be triggered when this signal becomes aborted.\n */\n addEventListener(type: 'abort', listener: () => void): void;\n\n /**\n * Remove an event listener that was previously added with {@link AbortSignal.addEventListener}.\n */\n removeEventListener(type: 'abort', listener: () => void): void;\n}\n\nexport function isAbortSignal(value: unknown): value is AbortSignal {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n try {\n return typeof (value as AbortSignal).aborted === 'boolean';\n } catch {\n // AbortSignal.prototype.aborted throws if its brand check fails\n return false;\n }\n}\n","import assert from '../stub/assert';\nimport {\n promiseRejectedWith,\n promiseResolvedWith,\n setPromiseIsHandledToTrue,\n transformPromiseWith\n} from './helpers/webidl';\nimport { QueuingStrategy, QueuingStrategySizeCallback } from './queuing-strategy';\nimport { AcquireReadableStreamAsyncIterator, ReadableStreamAsyncIterator } from './readable-stream/async-iterator';\nimport { defaultReaderClosedPromiseReject, defaultReaderClosedPromiseResolve } from './readable-stream/generic-reader';\nimport {\n AcquireReadableStreamDefaultReader,\n IsReadableStreamDefaultReader,\n ReadableStreamDefaultReader,\n ReadableStreamDefaultReadResult\n} from './readable-stream/default-reader';\nimport {\n AcquireReadableStreamBYOBReader,\n IsReadableStreamBYOBReader,\n ReadableStreamBYOBReader,\n ReadableStreamBYOBReadResult\n} from './readable-stream/byob-reader';\nimport { ReadableStreamPipeTo } from './readable-stream/pipe';\nimport { ReadableStreamTee } from './readable-stream/tee';\nimport { IsWritableStream, IsWritableStreamLocked, WritableStream } from './writable-stream';\nimport NumberIsInteger from '../stub/number-isinteger';\nimport { SimpleQueue } from './simple-queue';\nimport {\n ReadableByteStreamController,\n ReadableStreamBYOBRequest,\n SetUpReadableByteStreamController,\n SetUpReadableByteStreamControllerFromUnderlyingSource\n} from './readable-stream/byte-stream-controller';\nimport {\n ReadableStreamDefaultController,\n SetUpReadableStreamDefaultController,\n SetUpReadableStreamDefaultControllerFromUnderlyingSource\n} from './readable-stream/default-controller';\nimport {\n UnderlyingByteSource,\n UnderlyingByteSourcePullCallback,\n UnderlyingByteSourceStartCallback,\n UnderlyingSource,\n UnderlyingSourceCancelCallback,\n UnderlyingSourcePullCallback,\n UnderlyingSourceStartCallback\n} from './readable-stream/underlying-source';\nimport { noop } from '../utils';\nimport { typeIsObject } from './helpers/miscellaneous';\nimport { CreateArrayFromList } from './abstract-ops/ecmascript';\nimport { CancelSteps } from './abstract-ops/internal-methods';\nimport { IsNonNegativeNumber } from './abstract-ops/miscellaneous';\nimport { assertObject, assertRequiredArgument } from './validators/basic';\nimport { convertQueuingStrategy } from './validators/queuing-strategy';\nimport { ExtractHighWaterMark, ExtractSizeAlgorithm } from './abstract-ops/queuing-strategy';\nimport { convertUnderlyingDefaultOrByteSource } from './validators/underlying-source';\nimport { ReadableStreamGetReaderOptions } from './readable-stream/reader-options';\nimport { convertReaderOptions } from './validators/reader-options';\nimport { StreamPipeOptions, ValidatedStreamPipeOptions } from './readable-stream/pipe-options';\nimport { ReadableStreamIteratorOptions } from './readable-stream/iterator-options';\nimport { convertIteratorOptions } from './validators/iterator-options';\nimport { convertPipeOptions } from './validators/pipe-options';\nimport { ReadableWritablePair } from './readable-stream/readable-writable-pair';\nimport { convertReadableWritablePair } from './validators/readable-writable-pair';\n\nexport type ReadableByteStream = ReadableStream;\n\ntype ReadableStreamState = 'readable' | 'closed' | 'errored';\n\n/**\n * A readable stream represents a source of data, from which you can read.\n *\n * @public\n */\nexport class ReadableStream {\n /** @internal */\n _state!: ReadableStreamState;\n /** @internal */\n _reader: ReadableStreamReader | undefined;\n /** @internal */\n _storedError: any;\n /** @internal */\n _disturbed!: boolean;\n /** @internal */\n _readableStreamController!: ReadableStreamDefaultController | ReadableByteStreamController;\n\n constructor(underlyingSource: UnderlyingByteSource, strategy?: { highWaterMark?: number; size?: undefined });\n constructor(underlyingSource?: UnderlyingSource, strategy?: QueuingStrategy);\n constructor(rawUnderlyingSource: UnderlyingSource | UnderlyingByteSource | null | undefined = {},\n rawStrategy: QueuingStrategy | null | undefined = {}) {\n if (rawUnderlyingSource === undefined) {\n rawUnderlyingSource = null;\n } else {\n assertObject(rawUnderlyingSource, 'First parameter');\n }\n\n const strategy = convertQueuingStrategy(rawStrategy, 'Second parameter');\n const underlyingSource = convertUnderlyingDefaultOrByteSource(rawUnderlyingSource, 'First parameter');\n\n InitializeReadableStream(this);\n\n if (underlyingSource.type === 'bytes') {\n if (strategy.size !== undefined) {\n throw new RangeError('The strategy for a byte stream cannot have a size function');\n }\n const highWaterMark = ExtractHighWaterMark(strategy, 0);\n SetUpReadableByteStreamControllerFromUnderlyingSource(\n this as unknown as ReadableByteStream,\n underlyingSource,\n highWaterMark\n );\n } else {\n assert(underlyingSource.type === undefined);\n const sizeAlgorithm = ExtractSizeAlgorithm(strategy);\n const highWaterMark = ExtractHighWaterMark(strategy, 1);\n SetUpReadableStreamDefaultControllerFromUnderlyingSource(\n this,\n underlyingSource,\n highWaterMark,\n sizeAlgorithm\n );\n }\n }\n\n /**\n * Whether or not the readable stream is locked to a {@link ReadableStreamDefaultReader | reader}.\n */\n get locked(): boolean {\n if (!IsReadableStream(this)) {\n throw streamBrandCheckException('locked');\n }\n\n return IsReadableStreamLocked(this);\n }\n\n /**\n * Cancels the stream, signaling a loss of interest in the stream by a consumer.\n *\n * The supplied `reason` argument will be given to the underlying source's {@link UnderlyingSource.cancel | cancel()}\n * method, which might or might not use it.\n */\n cancel(reason: any = undefined): Promise {\n if (!IsReadableStream(this)) {\n return promiseRejectedWith(streamBrandCheckException('cancel'));\n }\n\n if (IsReadableStreamLocked(this)) {\n return promiseRejectedWith(new TypeError('Cannot cancel a stream that already has a reader'));\n }\n\n return ReadableStreamCancel(this, reason);\n }\n\n /**\n * Creates a {@link ReadableStreamBYOBReader} and locks the stream to the new reader.\n *\n * This call behaves the same way as the no-argument variant, except that it only works on readable byte streams,\n * i.e. streams which were constructed specifically with the ability to handle \"bring your own buffer\" reading.\n * The returned BYOB reader provides the ability to directly read individual chunks from the stream via its\n * {@link ReadableStreamBYOBReader.read | read()} method, into developer-supplied buffers, allowing more precise\n * control over allocation.\n */\n getReader({ mode }: { mode: 'byob' }): ReadableStreamBYOBReader;\n /**\n * Creates a {@link ReadableStreamDefaultReader} and locks the stream to the new reader.\n * While the stream is locked, no other reader can be acquired until this one is released.\n *\n * This functionality is especially useful for creating abstractions that desire the ability to consume a stream\n * in its entirety. By getting a reader for the stream, you can ensure nobody else can interleave reads with yours\n * or cancel the stream, which would interfere with your abstraction.\n */\n getReader(): ReadableStreamDefaultReader;\n getReader(\n rawOptions: ReadableStreamGetReaderOptions | null | undefined = undefined\n ): ReadableStreamDefaultReader | ReadableStreamBYOBReader {\n if (!IsReadableStream(this)) {\n throw streamBrandCheckException('getReader');\n }\n\n const options = convertReaderOptions(rawOptions, 'First parameter');\n\n if (options.mode === undefined) {\n return AcquireReadableStreamDefaultReader(this);\n }\n\n assert(options.mode === 'byob');\n return AcquireReadableStreamBYOBReader(this as unknown as ReadableByteStream);\n }\n\n /**\n * Provides a convenient, chainable way of piping this readable stream through a transform stream\n * (or any other `{ writable, readable }` pair). It simply {@link ReadableStream.pipeTo | pipes} the stream\n * into the writable side of the supplied pair, and returns the readable side for further use.\n *\n * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.\n */\n pipeThrough(transform: ReadableWritablePair, options?: StreamPipeOptions): ReadableStream;\n pipeThrough(rawTransform: ReadableWritablePair | null | undefined,\n rawOptions: StreamPipeOptions | null | undefined = {}): ReadableStream {\n if (!IsReadableStream(this)) {\n throw streamBrandCheckException('pipeThrough');\n }\n assertRequiredArgument(rawTransform, 1, 'pipeThrough');\n\n const transform = convertReadableWritablePair(rawTransform, 'First parameter');\n const options = convertPipeOptions(rawOptions, 'Second parameter');\n\n if (IsReadableStreamLocked(this)) {\n throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream');\n }\n if (IsWritableStreamLocked(transform.writable)) {\n throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream');\n }\n\n const promise = ReadableStreamPipeTo(\n this, transform.writable, options.preventClose, options.preventAbort, options.preventCancel, options.signal\n );\n\n setPromiseIsHandledToTrue(promise);\n\n return transform.readable;\n }\n\n /**\n * Pipes this readable stream to a given writable stream. The way in which the piping process behaves under\n * various error conditions can be customized with a number of passed options. It returns a promise that fulfills\n * when the piping process completes successfully, or rejects if any errors were encountered.\n *\n * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.\n */\n pipeTo(destination: WritableStream, options?: StreamPipeOptions): Promise;\n pipeTo(destination: WritableStream | null | undefined,\n rawOptions: StreamPipeOptions | null | undefined = {}): Promise {\n if (!IsReadableStream(this)) {\n return promiseRejectedWith(streamBrandCheckException('pipeTo'));\n }\n\n if (destination === undefined) {\n return promiseRejectedWith(`Parameter 1 is required in 'pipeTo'.`);\n }\n if (!IsWritableStream(destination)) {\n return promiseRejectedWith(\n new TypeError(`ReadableStream.prototype.pipeTo's first argument must be a WritableStream`)\n );\n }\n\n let options: ValidatedStreamPipeOptions;\n try {\n options = convertPipeOptions(rawOptions, 'Second parameter');\n } catch (e) {\n return promiseRejectedWith(e);\n }\n\n if (IsReadableStreamLocked(this)) {\n return promiseRejectedWith(\n new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream')\n );\n }\n if (IsWritableStreamLocked(destination)) {\n return promiseRejectedWith(\n new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream')\n );\n }\n\n return ReadableStreamPipeTo(\n this, destination, options.preventClose, options.preventAbort, options.preventCancel, options.signal\n );\n }\n\n /**\n * Tees this readable stream, returning a two-element array containing the two resulting branches as\n * new {@link ReadableStream} instances.\n *\n * Teeing a stream will lock it, preventing any other consumer from acquiring a reader.\n * To cancel the stream, cancel both of the resulting branches; a composite cancellation reason will then be\n * propagated to the stream's underlying source.\n *\n * Note that the chunks seen in each branch will be the same object. If the chunks are not immutable,\n * this could allow interference between the two branches.\n */\n tee(): [ReadableStream, ReadableStream] {\n if (!IsReadableStream(this)) {\n throw streamBrandCheckException('tee');\n }\n\n const branches = ReadableStreamTee(this, false);\n return CreateArrayFromList(branches);\n }\n\n /**\n * Asynchronously iterates over the chunks in the stream's internal queue.\n *\n * Asynchronously iterating over the stream will lock it, preventing any other consumer from acquiring a reader.\n * The lock will be released if the async iterator's {@link ReadableStreamAsyncIterator.return | return()} method\n * is called, e.g. by breaking out of the loop.\n *\n * By default, calling the async iterator's {@link ReadableStreamAsyncIterator.return | return()} method will also\n * cancel the stream. To prevent this, use the stream's {@link ReadableStream.values | values()} method, passing\n * `true` for the `preventCancel` option.\n */\n values(options?: ReadableStreamIteratorOptions): ReadableStreamAsyncIterator;\n values(rawOptions: ReadableStreamIteratorOptions | null | undefined = undefined): ReadableStreamAsyncIterator {\n if (!IsReadableStream(this)) {\n throw streamBrandCheckException('values');\n }\n\n const options = convertIteratorOptions(rawOptions, 'First parameter');\n return AcquireReadableStreamAsyncIterator(this, options.preventCancel);\n }\n\n /**\n * {@inheritDoc ReadableStream.values}\n */\n [Symbol.asyncIterator]: (options?: ReadableStreamIteratorOptions) => ReadableStreamAsyncIterator;\n}\n\nObject.defineProperties(ReadableStream.prototype, {\n cancel: { enumerable: true },\n getReader: { enumerable: true },\n pipeThrough: { enumerable: true },\n pipeTo: { enumerable: true },\n tee: { enumerable: true },\n values: { enumerable: true },\n locked: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(ReadableStream.prototype, Symbol.toStringTag, {\n value: 'ReadableStream',\n configurable: true\n });\n}\nif (typeof Symbol.asyncIterator === 'symbol') {\n Object.defineProperty(ReadableStream.prototype, Symbol.asyncIterator, {\n value: ReadableStream.prototype.values,\n writable: true,\n configurable: true\n });\n}\n\nexport {\n ReadableStreamAsyncIterator,\n ReadableStreamDefaultReadResult,\n ReadableStreamBYOBReadResult,\n UnderlyingByteSource,\n UnderlyingSource,\n UnderlyingSourceStartCallback,\n UnderlyingSourcePullCallback,\n UnderlyingSourceCancelCallback,\n UnderlyingByteSourceStartCallback,\n UnderlyingByteSourcePullCallback,\n StreamPipeOptions,\n ReadableWritablePair,\n ReadableStreamIteratorOptions\n};\n\n// Abstract operations for the ReadableStream.\n\n// Throws if and only if startAlgorithm throws.\nexport function CreateReadableStream(startAlgorithm: () => void | PromiseLike,\n pullAlgorithm: () => Promise,\n cancelAlgorithm: (reason: any) => Promise,\n highWaterMark = 1,\n sizeAlgorithm: QueuingStrategySizeCallback = () => 1): ReadableStream {\n assert(IsNonNegativeNumber(highWaterMark));\n\n const stream: ReadableStream = Object.create(ReadableStream.prototype);\n InitializeReadableStream(stream);\n\n const controller: ReadableStreamDefaultController = Object.create(ReadableStreamDefaultController.prototype);\n SetUpReadableStreamDefaultController(\n stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm\n );\n\n return stream;\n}\n\n// Throws if and only if startAlgorithm throws.\nexport function CreateReadableByteStream(startAlgorithm: () => void | PromiseLike,\n pullAlgorithm: () => Promise,\n cancelAlgorithm: (reason: any) => Promise,\n highWaterMark = 0,\n autoAllocateChunkSize: number | undefined = undefined): ReadableStream {\n assert(IsNonNegativeNumber(highWaterMark));\n if (autoAllocateChunkSize !== undefined) {\n assert(NumberIsInteger(autoAllocateChunkSize));\n assert(autoAllocateChunkSize > 0);\n }\n\n const stream: ReadableStream = Object.create(ReadableStream.prototype);\n InitializeReadableStream(stream);\n\n const controller: ReadableByteStreamController = Object.create(ReadableByteStreamController.prototype);\n\n SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark,\n autoAllocateChunkSize);\n\n return stream;\n}\n\nfunction InitializeReadableStream(stream: ReadableStream) {\n stream._state = 'readable';\n stream._reader = undefined;\n stream._storedError = undefined;\n stream._disturbed = false;\n}\n\nexport function IsReadableStream(x: unknown): x is ReadableStream {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_readableStreamController')) {\n return false;\n }\n\n return true;\n}\n\nexport function IsReadableStreamDisturbed(stream: ReadableStream): boolean {\n assert(IsReadableStream(stream));\n\n return stream._disturbed;\n}\n\nexport function IsReadableStreamLocked(stream: ReadableStream): boolean {\n assert(IsReadableStream(stream));\n\n if (stream._reader === undefined) {\n return false;\n }\n\n return true;\n}\n\n// ReadableStream API exposed for controllers.\n\nexport function ReadableStreamCancel(stream: ReadableStream, reason: any): Promise {\n stream._disturbed = true;\n\n if (stream._state === 'closed') {\n return promiseResolvedWith(undefined);\n }\n if (stream._state === 'errored') {\n return promiseRejectedWith(stream._storedError);\n }\n\n ReadableStreamClose(stream);\n\n const sourceCancelPromise = stream._readableStreamController[CancelSteps](reason);\n return transformPromiseWith(sourceCancelPromise, noop);\n}\n\nexport function ReadableStreamClose(stream: ReadableStream): void {\n assert(stream._state === 'readable');\n\n stream._state = 'closed';\n\n const reader = stream._reader;\n\n if (reader === undefined) {\n return;\n }\n\n if (IsReadableStreamDefaultReader(reader)) {\n reader._readRequests.forEach(readRequest => {\n readRequest._closeSteps();\n });\n reader._readRequests = new SimpleQueue();\n }\n\n defaultReaderClosedPromiseResolve(reader);\n}\n\nexport function ReadableStreamError(stream: ReadableStream, e: any): void {\n assert(IsReadableStream(stream));\n assert(stream._state === 'readable');\n\n stream._state = 'errored';\n stream._storedError = e;\n\n const reader = stream._reader;\n\n if (reader === undefined) {\n return;\n }\n\n if (IsReadableStreamDefaultReader(reader)) {\n reader._readRequests.forEach(readRequest => {\n readRequest._errorSteps(e);\n });\n\n reader._readRequests = new SimpleQueue();\n } else {\n assert(IsReadableStreamBYOBReader(reader));\n\n reader._readIntoRequests.forEach(readIntoRequest => {\n readIntoRequest._errorSteps(e);\n });\n\n reader._readIntoRequests = new SimpleQueue();\n }\n\n defaultReaderClosedPromiseReject(reader, e);\n}\n\n// Readers\n\nexport type ReadableStreamReader = ReadableStreamDefaultReader | ReadableStreamBYOBReader;\n\nexport {\n ReadableStreamDefaultReader,\n ReadableStreamBYOBReader\n};\n\n// Controllers\n\nexport {\n ReadableStreamDefaultController,\n ReadableStreamBYOBRequest,\n ReadableByteStreamController\n};\n\n// Helper functions for the ReadableStream.\n\nfunction streamBrandCheckException(name: string): TypeError {\n return new TypeError(`ReadableStream.prototype.${name} can only be used on a ReadableStream`);\n}\n","import { assertDictionary, assertRequiredField } from './basic';\nimport { ReadableWritablePair } from '../readable-stream/readable-writable-pair';\nimport { assertReadableStream } from './readable-stream';\nimport { assertWritableStream } from './writable-stream';\n\nexport function convertReadableWritablePair(pair: ReadableWritablePair | null | undefined,\n context: string): ReadableWritablePair {\n assertDictionary(pair, context);\n\n const readable = pair?.readable;\n assertRequiredField(readable, 'readable', 'ReadableWritablePair');\n assertReadableStream(readable, `${context} has member 'readable' that`);\n\n const writable = pair?.writable;\n assertRequiredField(writable, 'writable', 'ReadableWritablePair');\n assertWritableStream(writable, `${context} has member 'writable' that`);\n\n return { readable, writable };\n}\n","import { CreateReadableStream, IsReadableStream, ReadableStream, ReadableStreamCancel } from '../readable-stream';\nimport { AcquireReadableStreamDefaultReader, ReadableStreamDefaultReaderRead, ReadRequest } from './default-reader';\nimport assert from '../../stub/assert';\nimport { newPromise, promiseResolvedWith, queueMicrotask, uponRejection } from '../helpers/webidl';\nimport {\n ReadableStreamDefaultController,\n ReadableStreamDefaultControllerClose,\n ReadableStreamDefaultControllerEnqueue,\n ReadableStreamDefaultControllerError\n} from './default-controller';\nimport { CreateArrayFromList } from '../abstract-ops/ecmascript';\n\nexport function ReadableStreamTee(stream: ReadableStream,\n cloneForBranch2: boolean): [ReadableStream, ReadableStream] {\n assert(IsReadableStream(stream));\n assert(typeof cloneForBranch2 === 'boolean');\n\n const reader = AcquireReadableStreamDefaultReader(stream);\n\n let reading = false;\n let canceled1 = false;\n let canceled2 = false;\n let reason1: any;\n let reason2: any;\n let branch1: ReadableStream;\n let branch2: ReadableStream;\n\n let resolveCancelPromise: (reason: any) => void;\n const cancelPromise = newPromise(resolve => {\n resolveCancelPromise = resolve;\n });\n\n function pullAlgorithm(): Promise {\n if (reading) {\n return promiseResolvedWith(undefined);\n }\n\n reading = true;\n\n const readRequest: ReadRequest = {\n _chunkSteps: value => {\n // This needs to be delayed a microtask because it takes at least a microtask to detect errors (using\n // reader._closedPromise below), and we want errors in stream to error both branches immediately. We cannot let\n // successful synchronously-available reads get ahead of asynchronously-available errors.\n queueMicrotask(() => {\n reading = false;\n const value1 = value;\n const value2 = value;\n\n // There is no way to access the cloning code right now in the reference implementation.\n // If we add one then we'll need an implementation for serializable objects.\n // if (!canceled2 && cloneForBranch2) {\n // value2 = StructuredDeserialize(StructuredSerialize(value2));\n // }\n\n if (!canceled1) {\n ReadableStreamDefaultControllerEnqueue(\n branch1._readableStreamController as ReadableStreamDefaultController,\n value1\n );\n }\n\n if (!canceled2) {\n ReadableStreamDefaultControllerEnqueue(\n branch2._readableStreamController as ReadableStreamDefaultController,\n value2\n );\n }\n\n resolveCancelPromise(undefined);\n });\n },\n _closeSteps: () => {\n reading = false;\n if (!canceled1) {\n ReadableStreamDefaultControllerClose(branch1._readableStreamController as ReadableStreamDefaultController);\n }\n if (!canceled2) {\n ReadableStreamDefaultControllerClose(branch2._readableStreamController as ReadableStreamDefaultController);\n }\n },\n _errorSteps: () => {\n reading = false;\n }\n };\n ReadableStreamDefaultReaderRead(reader, readRequest);\n\n return promiseResolvedWith(undefined);\n }\n\n function cancel1Algorithm(reason: any): Promise {\n canceled1 = true;\n reason1 = reason;\n if (canceled2) {\n const compositeReason = CreateArrayFromList([reason1, reason2]);\n const cancelResult = ReadableStreamCancel(stream, compositeReason);\n resolveCancelPromise(cancelResult);\n }\n return cancelPromise;\n }\n\n function cancel2Algorithm(reason: any): Promise {\n canceled2 = true;\n reason2 = reason;\n if (canceled1) {\n const compositeReason = CreateArrayFromList([reason1, reason2]);\n const cancelResult = ReadableStreamCancel(stream, compositeReason);\n resolveCancelPromise(cancelResult);\n }\n return cancelPromise;\n }\n\n function startAlgorithm() {\n // do nothing\n }\n\n branch1 = CreateReadableStream(startAlgorithm, pullAlgorithm, cancel1Algorithm);\n branch2 = CreateReadableStream(startAlgorithm, pullAlgorithm, cancel2Algorithm);\n\n uponRejection(reader._closedPromise, (r: any) => {\n ReadableStreamDefaultControllerError(branch1._readableStreamController as ReadableStreamDefaultController, r);\n ReadableStreamDefaultControllerError(branch2._readableStreamController as ReadableStreamDefaultController, r);\n resolveCancelPromise(undefined);\n });\n\n return [branch1, branch2];\n}\n","import { assertDictionary } from './basic';\nimport {\n ReadableStreamIteratorOptions,\n ValidatedReadableStreamIteratorOptions\n} from '../readable-stream/iterator-options';\n\nexport function convertIteratorOptions(options: ReadableStreamIteratorOptions | null | undefined,\n context: string): ValidatedReadableStreamIteratorOptions {\n assertDictionary(options, context);\n const preventCancel = options?.preventCancel;\n return { preventCancel: Boolean(preventCancel) };\n}\n","import { QueuingStrategyInit } from '../queuing-strategy';\nimport { assertDictionary, assertRequiredField, convertUnrestrictedDouble } from './basic';\n\nexport function convertQueuingStrategyInit(init: QueuingStrategyInit | null | undefined,\n context: string): QueuingStrategyInit {\n assertDictionary(init, context);\n const highWaterMark = init?.highWaterMark;\n assertRequiredField(highWaterMark, 'highWaterMark', 'QueuingStrategyInit');\n return {\n highWaterMark: convertUnrestrictedDouble(highWaterMark)\n };\n}\n","import { QueuingStrategy, QueuingStrategyInit } from './queuing-strategy';\nimport { typeIsObject } from './helpers/miscellaneous';\nimport { assertRequiredArgument } from './validators/basic';\nimport { convertQueuingStrategyInit } from './validators/queuing-strategy-init';\n\nconst byteLengthSizeFunction = function size(chunk: ArrayBufferView): number {\n return chunk.byteLength;\n};\n\n/**\n * A queuing strategy that counts the number of bytes in each chunk.\n *\n * @public\n */\nexport default class ByteLengthQueuingStrategy implements QueuingStrategy {\n /** @internal */\n readonly _byteLengthQueuingStrategyHighWaterMark: number;\n\n constructor(options: QueuingStrategyInit) {\n assertRequiredArgument(options, 1, 'ByteLengthQueuingStrategy');\n options = convertQueuingStrategyInit(options, 'First parameter');\n this._byteLengthQueuingStrategyHighWaterMark = options.highWaterMark;\n }\n\n /**\n * Returns the high water mark provided to the constructor.\n */\n get highWaterMark(): number {\n if (!IsByteLengthQueuingStrategy(this)) {\n throw byteLengthBrandCheckException('highWaterMark');\n }\n return this._byteLengthQueuingStrategyHighWaterMark;\n }\n\n /**\n * Measures the size of `chunk` by returning the value of its `byteLength` property.\n */\n get size(): (chunk: ArrayBufferView) => number {\n if (!IsByteLengthQueuingStrategy(this)) {\n throw byteLengthBrandCheckException('size');\n }\n return byteLengthSizeFunction;\n }\n}\n\nObject.defineProperties(ByteLengthQueuingStrategy.prototype, {\n highWaterMark: { enumerable: true },\n size: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(ByteLengthQueuingStrategy.prototype, Symbol.toStringTag, {\n value: 'ByteLengthQueuingStrategy',\n configurable: true\n });\n}\n\n// Helper functions for the ByteLengthQueuingStrategy.\n\nfunction byteLengthBrandCheckException(name: string): TypeError {\n return new TypeError(`ByteLengthQueuingStrategy.prototype.${name} can only be used on a ByteLengthQueuingStrategy`);\n}\n\nexport function IsByteLengthQueuingStrategy(x: any): x is ByteLengthQueuingStrategy {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_byteLengthQueuingStrategyHighWaterMark')) {\n return false;\n }\n\n return true;\n}\n","import { QueuingStrategy, QueuingStrategyInit } from './queuing-strategy';\nimport { typeIsObject } from './helpers/miscellaneous';\nimport { assertRequiredArgument } from './validators/basic';\nimport { convertQueuingStrategyInit } from './validators/queuing-strategy-init';\n\nconst countSizeFunction = function size(): 1 {\n return 1;\n};\n\n/**\n * A queuing strategy that counts the number of chunks.\n *\n * @public\n */\nexport default class CountQueuingStrategy implements QueuingStrategy {\n /** @internal */\n readonly _countQueuingStrategyHighWaterMark!: number;\n\n constructor(options: QueuingStrategyInit) {\n assertRequiredArgument(options, 1, 'CountQueuingStrategy');\n options = convertQueuingStrategyInit(options, 'First parameter');\n this._countQueuingStrategyHighWaterMark = options.highWaterMark;\n }\n\n /**\n * Returns the high water mark provided to the constructor.\n */\n get highWaterMark(): number {\n if (!IsCountQueuingStrategy(this)) {\n throw countBrandCheckException('highWaterMark');\n }\n return this._countQueuingStrategyHighWaterMark;\n }\n\n /**\n * Measures the size of `chunk` by always returning 1.\n * This ensures that the total queue size is a count of the number of chunks in the queue.\n */\n get size(): (chunk: any) => 1 {\n if (!IsCountQueuingStrategy(this)) {\n throw countBrandCheckException('size');\n }\n return countSizeFunction;\n }\n}\n\nObject.defineProperties(CountQueuingStrategy.prototype, {\n highWaterMark: { enumerable: true },\n size: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(CountQueuingStrategy.prototype, Symbol.toStringTag, {\n value: 'CountQueuingStrategy',\n configurable: true\n });\n}\n\n// Helper functions for the CountQueuingStrategy.\n\nfunction countBrandCheckException(name: string): TypeError {\n return new TypeError(`CountQueuingStrategy.prototype.${name} can only be used on a CountQueuingStrategy`);\n}\n\nexport function IsCountQueuingStrategy(x: any): x is CountQueuingStrategy {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_countQueuingStrategyHighWaterMark')) {\n return false;\n }\n\n return true;\n}\n","import { assertDictionary, assertFunction } from './basic';\nimport { promiseCall, reflectCall } from '../helpers/webidl';\nimport {\n Transformer,\n TransformerFlushCallback,\n TransformerStartCallback,\n TransformerTransformCallback,\n ValidatedTransformer\n} from '../transform-stream/transformer';\nimport { TransformStreamDefaultController } from '../transform-stream';\n\nexport function convertTransformer(original: Transformer | null,\n context: string): ValidatedTransformer {\n assertDictionary(original, context);\n const flush = original?.flush;\n const readableType = original?.readableType;\n const start = original?.start;\n const transform = original?.transform;\n const writableType = original?.writableType;\n return {\n flush: flush === undefined ?\n undefined :\n convertTransformerFlushCallback(flush, original!, `${context} has member 'flush' that`),\n readableType,\n start: start === undefined ?\n undefined :\n convertTransformerStartCallback(start, original!, `${context} has member 'start' that`),\n transform: transform === undefined ?\n undefined :\n convertTransformerTransformCallback(transform, original!, `${context} has member 'transform' that`),\n writableType\n };\n}\n\nfunction convertTransformerFlushCallback(\n fn: TransformerFlushCallback,\n original: Transformer,\n context: string\n): (controller: TransformStreamDefaultController) => Promise {\n assertFunction(fn, context);\n return (controller: TransformStreamDefaultController) => promiseCall(fn, original, [controller]);\n}\n\nfunction convertTransformerStartCallback(\n fn: TransformerStartCallback,\n original: Transformer,\n context: string\n): TransformerStartCallback {\n assertFunction(fn, context);\n return (controller: TransformStreamDefaultController) => reflectCall(fn, original, [controller]);\n}\n\nfunction convertTransformerTransformCallback(\n fn: TransformerTransformCallback,\n original: Transformer,\n context: string\n): (chunk: I, controller: TransformStreamDefaultController) => Promise {\n assertFunction(fn, context);\n return (chunk: I, controller: TransformStreamDefaultController) => promiseCall(fn, original, [chunk, controller]);\n}\n","import assert from '../stub/assert';\nimport { newPromise, promiseRejectedWith, promiseResolvedWith, transformPromiseWith } from './helpers/webidl';\nimport { CreateReadableStream, ReadableStream, ReadableStreamDefaultController } from './readable-stream';\nimport {\n ReadableStreamDefaultControllerCanCloseOrEnqueue,\n ReadableStreamDefaultControllerClose,\n ReadableStreamDefaultControllerEnqueue,\n ReadableStreamDefaultControllerError,\n ReadableStreamDefaultControllerGetDesiredSize,\n ReadableStreamDefaultControllerHasBackpressure\n} from './readable-stream/default-controller';\nimport { QueuingStrategy, QueuingStrategySizeCallback } from './queuing-strategy';\nimport { CreateWritableStream, WritableStream, WritableStreamDefaultControllerErrorIfNeeded } from './writable-stream';\nimport { typeIsObject } from './helpers/miscellaneous';\nimport { IsNonNegativeNumber } from './abstract-ops/miscellaneous';\nimport { convertQueuingStrategy } from './validators/queuing-strategy';\nimport { ExtractHighWaterMark, ExtractSizeAlgorithm } from './abstract-ops/queuing-strategy';\nimport {\n Transformer,\n TransformerFlushCallback,\n TransformerStartCallback,\n TransformerTransformCallback,\n ValidatedTransformer\n} from './transform-stream/transformer';\nimport { convertTransformer } from './validators/transformer';\n\n// Class TransformStream\n\n/**\n * A transform stream consists of a pair of streams: a {@link WritableStream | writable stream},\n * known as its writable side, and a {@link ReadableStream | readable stream}, known as its readable side.\n * In a manner specific to the transform stream in question, writes to the writable side result in new data being\n * made available for reading from the readable side.\n *\n * @public\n */\nexport class TransformStream {\n /** @internal */\n _writable!: WritableStream;\n /** @internal */\n _readable!: ReadableStream;\n /** @internal */\n _backpressure!: boolean;\n /** @internal */\n _backpressureChangePromise!: Promise;\n /** @internal */\n _backpressureChangePromise_resolve!: () => void;\n /** @internal */\n _transformStreamController!: TransformStreamDefaultController;\n\n constructor(\n transformer?: Transformer,\n writableStrategy?: QueuingStrategy,\n readableStrategy?: QueuingStrategy\n );\n constructor(rawTransformer: Transformer | null | undefined = {},\n rawWritableStrategy: QueuingStrategy | null | undefined = {},\n rawReadableStrategy: QueuingStrategy | null | undefined = {}) {\n if (rawTransformer === undefined) {\n rawTransformer = null;\n }\n\n const writableStrategy = convertQueuingStrategy(rawWritableStrategy, 'Second parameter');\n const readableStrategy = convertQueuingStrategy(rawReadableStrategy, 'Third parameter');\n\n const transformer = convertTransformer(rawTransformer, 'First parameter');\n if (transformer.readableType !== undefined) {\n throw new RangeError('Invalid readableType specified');\n }\n if (transformer.writableType !== undefined) {\n throw new RangeError('Invalid writableType specified');\n }\n\n const readableHighWaterMark = ExtractHighWaterMark(readableStrategy, 0);\n const readableSizeAlgorithm = ExtractSizeAlgorithm(readableStrategy);\n const writableHighWaterMark = ExtractHighWaterMark(writableStrategy, 1);\n const writableSizeAlgorithm = ExtractSizeAlgorithm(writableStrategy);\n\n let startPromise_resolve!: (value: void | PromiseLike) => void;\n const startPromise = newPromise(resolve => {\n startPromise_resolve = resolve;\n });\n\n InitializeTransformStream(\n this, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm\n );\n SetUpTransformStreamDefaultControllerFromTransformer(this, transformer);\n\n if (transformer.start !== undefined) {\n startPromise_resolve(transformer.start(this._transformStreamController));\n } else {\n startPromise_resolve(undefined);\n }\n }\n\n /**\n * The readable side of the transform stream.\n */\n get readable(): ReadableStream {\n if (!IsTransformStream(this)) {\n throw streamBrandCheckException('readable');\n }\n\n return this._readable;\n }\n\n /**\n * The writable side of the transform stream.\n */\n get writable(): WritableStream {\n if (!IsTransformStream(this)) {\n throw streamBrandCheckException('writable');\n }\n\n return this._writable;\n }\n}\n\nObject.defineProperties(TransformStream.prototype, {\n readable: { enumerable: true },\n writable: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(TransformStream.prototype, Symbol.toStringTag, {\n value: 'TransformStream',\n configurable: true\n });\n}\n\nexport {\n Transformer,\n TransformerStartCallback,\n TransformerFlushCallback,\n TransformerTransformCallback\n};\n\n// Transform Stream Abstract Operations\n\nexport function CreateTransformStream(startAlgorithm: () => void | PromiseLike,\n transformAlgorithm: (chunk: I) => Promise,\n flushAlgorithm: () => Promise,\n writableHighWaterMark = 1,\n writableSizeAlgorithm: QueuingStrategySizeCallback = () => 1,\n readableHighWaterMark = 0,\n readableSizeAlgorithm: QueuingStrategySizeCallback = () => 1) {\n assert(IsNonNegativeNumber(writableHighWaterMark));\n assert(IsNonNegativeNumber(readableHighWaterMark));\n\n const stream: TransformStream = Object.create(TransformStream.prototype);\n\n let startPromise_resolve!: (value: void | PromiseLike) => void;\n const startPromise = newPromise(resolve => {\n startPromise_resolve = resolve;\n });\n\n InitializeTransformStream(stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark,\n readableSizeAlgorithm);\n\n const controller: TransformStreamDefaultController = Object.create(TransformStreamDefaultController.prototype);\n\n SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm);\n\n const startResult = startAlgorithm();\n startPromise_resolve(startResult);\n return stream;\n}\n\nfunction InitializeTransformStream(stream: TransformStream,\n startPromise: Promise,\n writableHighWaterMark: number,\n writableSizeAlgorithm: QueuingStrategySizeCallback,\n readableHighWaterMark: number,\n readableSizeAlgorithm: QueuingStrategySizeCallback) {\n function startAlgorithm(): Promise {\n return startPromise;\n }\n\n function writeAlgorithm(chunk: I): Promise {\n return TransformStreamDefaultSinkWriteAlgorithm(stream, chunk);\n }\n\n function abortAlgorithm(reason: any): Promise {\n return TransformStreamDefaultSinkAbortAlgorithm(stream, reason);\n }\n\n function closeAlgorithm(): Promise {\n return TransformStreamDefaultSinkCloseAlgorithm(stream);\n }\n\n stream._writable = CreateWritableStream(startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm,\n writableHighWaterMark, writableSizeAlgorithm);\n\n function pullAlgorithm(): Promise {\n return TransformStreamDefaultSourcePullAlgorithm(stream);\n }\n\n function cancelAlgorithm(reason: any): Promise {\n TransformStreamErrorWritableAndUnblockWrite(stream, reason);\n return promiseResolvedWith(undefined);\n }\n\n stream._readable = CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, readableHighWaterMark,\n readableSizeAlgorithm);\n\n // The [[backpressure]] slot is set to undefined so that it can be initialised by TransformStreamSetBackpressure.\n stream._backpressure = undefined!;\n stream._backpressureChangePromise = undefined!;\n stream._backpressureChangePromise_resolve = undefined!;\n TransformStreamSetBackpressure(stream, true);\n\n stream._transformStreamController = undefined!;\n}\n\nfunction IsTransformStream(x: unknown): x is TransformStream {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_transformStreamController')) {\n return false;\n }\n\n return true;\n}\n\n// This is a no-op if both sides are already errored.\nfunction TransformStreamError(stream: TransformStream, e: any) {\n ReadableStreamDefaultControllerError(stream._readable._readableStreamController as ReadableStreamDefaultController,\n e);\n TransformStreamErrorWritableAndUnblockWrite(stream, e);\n}\n\nfunction TransformStreamErrorWritableAndUnblockWrite(stream: TransformStream, e: any) {\n TransformStreamDefaultControllerClearAlgorithms(stream._transformStreamController);\n WritableStreamDefaultControllerErrorIfNeeded(stream._writable._writableStreamController, e);\n if (stream._backpressure) {\n // Pretend that pull() was called to permit any pending write() calls to complete. TransformStreamSetBackpressure()\n // cannot be called from enqueue() or pull() once the ReadableStream is errored, so this will will be the final time\n // _backpressure is set.\n TransformStreamSetBackpressure(stream, false);\n }\n}\n\nfunction TransformStreamSetBackpressure(stream: TransformStream, backpressure: boolean) {\n // Passes also when called during construction.\n assert(stream._backpressure !== backpressure);\n\n if (stream._backpressureChangePromise !== undefined) {\n stream._backpressureChangePromise_resolve();\n }\n\n stream._backpressureChangePromise = newPromise(resolve => {\n stream._backpressureChangePromise_resolve = resolve;\n });\n\n stream._backpressure = backpressure;\n}\n\n// Class TransformStreamDefaultController\n\n/**\n * Allows control of the {@link ReadableStream} and {@link WritableStream} of the associated {@link TransformStream}.\n *\n * @public\n */\nexport class TransformStreamDefaultController {\n /** @internal */\n _controlledTransformStream: TransformStream;\n /** @internal */\n _transformAlgorithm: (chunk: any) => Promise;\n /** @internal */\n _flushAlgorithm: () => Promise;\n\n private constructor() {\n throw new TypeError('Illegal constructor');\n }\n\n /**\n * Returns the desired size to fill the readable side’s internal queue. It can be negative, if the queue is over-full.\n */\n get desiredSize(): number | null {\n if (!IsTransformStreamDefaultController(this)) {\n throw defaultControllerBrandCheckException('desiredSize');\n }\n\n const readableController = this._controlledTransformStream._readable._readableStreamController;\n return ReadableStreamDefaultControllerGetDesiredSize(readableController as ReadableStreamDefaultController);\n }\n\n /**\n * Enqueues the given chunk `chunk` in the readable side of the controlled transform stream.\n */\n enqueue(chunk: O): void;\n enqueue(chunk: O = undefined!): void {\n if (!IsTransformStreamDefaultController(this)) {\n throw defaultControllerBrandCheckException('enqueue');\n }\n\n TransformStreamDefaultControllerEnqueue(this, chunk);\n }\n\n /**\n * Errors both the readable side and the writable side of the controlled transform stream, making all future\n * interactions with it fail with the given error `e`. Any chunks queued for transformation will be discarded.\n */\n error(reason: any = undefined): void {\n if (!IsTransformStreamDefaultController(this)) {\n throw defaultControllerBrandCheckException('error');\n }\n\n TransformStreamDefaultControllerError(this, reason);\n }\n\n /**\n * Closes the readable side and errors the writable side of the controlled transform stream. This is useful when the\n * transformer only needs to consume a portion of the chunks written to the writable side.\n */\n terminate(): void {\n if (!IsTransformStreamDefaultController(this)) {\n throw defaultControllerBrandCheckException('terminate');\n }\n\n TransformStreamDefaultControllerTerminate(this);\n }\n}\n\nObject.defineProperties(TransformStreamDefaultController.prototype, {\n enqueue: { enumerable: true },\n error: { enumerable: true },\n terminate: { enumerable: true },\n desiredSize: { enumerable: true }\n});\nif (typeof Symbol.toStringTag === 'symbol') {\n Object.defineProperty(TransformStreamDefaultController.prototype, Symbol.toStringTag, {\n value: 'TransformStreamDefaultController',\n configurable: true\n });\n}\n\n// Transform Stream Default Controller Abstract Operations\n\nfunction IsTransformStreamDefaultController(x: any): x is TransformStreamDefaultController {\n if (!typeIsObject(x)) {\n return false;\n }\n\n if (!Object.prototype.hasOwnProperty.call(x, '_controlledTransformStream')) {\n return false;\n }\n\n return true;\n}\n\nfunction SetUpTransformStreamDefaultController(stream: TransformStream,\n controller: TransformStreamDefaultController,\n transformAlgorithm: (chunk: I) => Promise,\n flushAlgorithm: () => Promise) {\n assert(IsTransformStream(stream));\n assert(stream._transformStreamController === undefined);\n\n controller._controlledTransformStream = stream;\n stream._transformStreamController = controller;\n\n controller._transformAlgorithm = transformAlgorithm;\n controller._flushAlgorithm = flushAlgorithm;\n}\n\nfunction SetUpTransformStreamDefaultControllerFromTransformer(stream: TransformStream,\n transformer: ValidatedTransformer) {\n const controller: TransformStreamDefaultController = Object.create(TransformStreamDefaultController.prototype);\n\n let transformAlgorithm = (chunk: I): Promise => {\n try {\n TransformStreamDefaultControllerEnqueue(controller, chunk as unknown as O);\n return promiseResolvedWith(undefined);\n } catch (transformResultE) {\n return promiseRejectedWith(transformResultE);\n }\n };\n\n let flushAlgorithm: () => Promise = () => promiseResolvedWith(undefined);\n\n if (transformer.transform !== undefined) {\n transformAlgorithm = chunk => transformer.transform!(chunk, controller);\n }\n if (transformer.flush !== undefined) {\n flushAlgorithm = () => transformer.flush!(controller);\n }\n\n SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm);\n}\n\nfunction TransformStreamDefaultControllerClearAlgorithms(controller: TransformStreamDefaultController) {\n controller._transformAlgorithm = undefined!;\n controller._flushAlgorithm = undefined!;\n}\n\nfunction TransformStreamDefaultControllerEnqueue(controller: TransformStreamDefaultController, chunk: O) {\n const stream = controller._controlledTransformStream;\n const readableController = stream._readable._readableStreamController as ReadableStreamDefaultController;\n if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(readableController)) {\n throw new TypeError('Readable side is not in a state that permits enqueue');\n }\n\n // We throttle transform invocations based on the backpressure of the ReadableStream, but we still\n // accept TransformStreamDefaultControllerEnqueue() calls.\n\n try {\n ReadableStreamDefaultControllerEnqueue(readableController, chunk);\n } catch (e) {\n // This happens when readableStrategy.size() throws.\n TransformStreamErrorWritableAndUnblockWrite(stream, e);\n\n throw stream._readable._storedError;\n }\n\n const backpressure = ReadableStreamDefaultControllerHasBackpressure(readableController);\n if (backpressure !== stream._backpressure) {\n assert(backpressure);\n TransformStreamSetBackpressure(stream, true);\n }\n}\n\nfunction TransformStreamDefaultControllerError(controller: TransformStreamDefaultController, e: any) {\n TransformStreamError(controller._controlledTransformStream, e);\n}\n\nfunction TransformStreamDefaultControllerPerformTransform(controller: TransformStreamDefaultController,\n chunk: I) {\n const transformPromise = controller._transformAlgorithm(chunk);\n return transformPromiseWith(transformPromise, undefined, r => {\n TransformStreamError(controller._controlledTransformStream, r);\n throw r;\n });\n}\n\nfunction TransformStreamDefaultControllerTerminate(controller: TransformStreamDefaultController) {\n const stream = controller._controlledTransformStream;\n const readableController = stream._readable._readableStreamController as ReadableStreamDefaultController;\n\n ReadableStreamDefaultControllerClose(readableController);\n\n const error = new TypeError('TransformStream terminated');\n TransformStreamErrorWritableAndUnblockWrite(stream, error);\n}\n\n// TransformStreamDefaultSink Algorithms\n\nfunction TransformStreamDefaultSinkWriteAlgorithm(stream: TransformStream, chunk: I): Promise {\n assert(stream._writable._state === 'writable');\n\n const controller = stream._transformStreamController;\n\n if (stream._backpressure) {\n const backpressureChangePromise = stream._backpressureChangePromise;\n assert(backpressureChangePromise !== undefined);\n return transformPromiseWith(backpressureChangePromise, () => {\n const writable = stream._writable;\n const state = writable._state;\n if (state === 'erroring') {\n throw writable._storedError;\n }\n assert(state === 'writable');\n return TransformStreamDefaultControllerPerformTransform(controller, chunk);\n });\n }\n\n return TransformStreamDefaultControllerPerformTransform(controller, chunk);\n}\n\nfunction TransformStreamDefaultSinkAbortAlgorithm(stream: TransformStream, reason: any): Promise {\n // abort() is not called synchronously, so it is possible for abort() to be called when the stream is already\n // errored.\n TransformStreamError(stream, reason);\n return promiseResolvedWith(undefined);\n}\n\nfunction TransformStreamDefaultSinkCloseAlgorithm(stream: TransformStream): Promise {\n // stream._readable cannot change after construction, so caching it across a call to user code is safe.\n const readable = stream._readable;\n\n const controller = stream._transformStreamController;\n const flushPromise = controller._flushAlgorithm();\n TransformStreamDefaultControllerClearAlgorithms(controller);\n\n // Return a promise that is fulfilled with undefined on success.\n return transformPromiseWith(flushPromise, () => {\n if (readable._state === 'errored') {\n throw readable._storedError;\n }\n ReadableStreamDefaultControllerClose(readable._readableStreamController as ReadableStreamDefaultController);\n }, r => {\n TransformStreamError(stream, r);\n throw readable._storedError;\n });\n}\n\n// TransformStreamDefaultSource Algorithms\n\nfunction TransformStreamDefaultSourcePullAlgorithm(stream: TransformStream): Promise {\n // Invariant. Enforced by the promises returned by start() and pull().\n assert(stream._backpressure);\n\n assert(stream._backpressureChangePromise !== undefined);\n\n TransformStreamSetBackpressure(stream, false);\n\n // Prevent the next pull() call until there is backpressure.\n return stream._backpressureChangePromise;\n}\n\n// Helper functions for the TransformStreamDefaultController.\n\nfunction defaultControllerBrandCheckException(name: string): TypeError {\n return new TypeError(\n `TransformStreamDefaultController.prototype.${name} can only be used on a TransformStreamDefaultController`);\n}\n\n// Helper functions for the TransformStream.\n\nfunction streamBrandCheckException(name: string): TypeError {\n return new TypeError(\n `TransformStream.prototype.${name} can only be used on a TransformStream`);\n}\n","import {\n ByteLengthQueuingStrategy,\n CountQueuingStrategy,\n ReadableByteStreamController,\n ReadableStream,\n ReadableStreamBYOBReader,\n ReadableStreamBYOBRequest,\n ReadableStreamDefaultController,\n ReadableStreamDefaultReader,\n TransformStream,\n TransformStreamDefaultController,\n WritableStream,\n WritableStreamDefaultController,\n WritableStreamDefaultWriter\n} from './ponyfill';\nimport { globals } from './utils';\n\n// Export\nexport * from './ponyfill';\n\nconst exports = {\n ReadableStream,\n ReadableStreamDefaultController,\n ReadableByteStreamController,\n ReadableStreamBYOBRequest,\n ReadableStreamDefaultReader,\n ReadableStreamBYOBReader,\n\n WritableStream,\n WritableStreamDefaultController,\n WritableStreamDefaultWriter,\n\n ByteLengthQueuingStrategy,\n CountQueuingStrategy,\n\n TransformStream,\n TransformStreamDefaultController\n};\n\n// Add classes to global scope\nif (typeof globals !== 'undefined') {\n for (const prop in exports) {\n if (Object.prototype.hasOwnProperty.call(exports, prop)) {\n Object.defineProperty(globals, prop, {\n value: exports[prop as (keyof typeof exports)],\n writable: true,\n configurable: true\n });\n }\n }\n}\n"],"names":["SymbolPolyfill","Symbol","iterator","description","noop","globals","self","window","global","typeIsObject","x","rethrowAssertionErrorRejection","originalPromise","Promise","originalPromiseThen","prototype","then","originalPromiseResolve","resolve","bind","originalPromiseReject","reject","newPromise","executor","promiseResolvedWith","value","promiseRejectedWith","reason","PerformPromiseThen","promise","onFulfilled","onRejected","call","uponPromise","undefined","uponFulfillment","uponRejection","transformPromiseWith","fulfillmentHandler","rejectionHandler","setPromiseIsHandledToTrue","queueMicrotask","globalQueueMicrotask","resolvedPromise","fn","reflectCall","F","V","args","TypeError","Function","apply","promiseCall","this","_front","_elements","_next","_back","_cursor","_size","Object","SimpleQueue","element","oldBack","newBack","QUEUE_MAX_ARRAY_SIZE","length","push","oldFront","newFront","oldCursor","newCursor","elements","callback","i","node","front","cursor","ReadableStreamReaderGenericInitialize","reader","stream","_ownerReadableStream","_reader","_state","defaultReaderClosedPromiseInitialize","defaultReaderClosedPromiseResolve","defaultReaderClosedPromiseInitializeAsResolved","defaultReaderClosedPromiseInitializeAsRejected","_storedError","ReadableStreamReaderGenericCancel","ReadableStreamCancel","ReadableStreamReaderGenericRelease","defaultReaderClosedPromiseReject","defaultReaderClosedPromiseResetToRejected","readerLockException","name","_closedPromise","_closedPromise_resolve","_closedPromise_reject","AbortSteps","ErrorSteps","CancelSteps","PullSteps","NumberIsFinite","Number","isFinite","MathTrunc","Math","trunc","v","ceil","floor","assertDictionary","obj","context","assertFunction","assertObject","isObject","assertRequiredArgument","position","assertRequiredField","field","convertUnrestrictedDouble","censorNegativeZero","convertUnsignedLongLongWithEnforceRange","upperBound","MAX_SAFE_INTEGER","integerPart","assertReadableStream","IsReadableStream","AcquireReadableStreamDefaultReader","ReadableStreamDefaultReader","ReadableStreamAddReadRequest","readRequest","_readRequests","ReadableStreamFulfillReadRequest","chunk","done","shift","_closeSteps","_chunkSteps","ReadableStreamGetNumReadRequests","ReadableStreamHasDefaultReader","IsReadableStreamDefaultReader","AsyncIteratorPrototype","IsReadableStreamLocked","defaultReaderBrandCheckException","resolvePromise","rejectPromise","ReadableStreamDefaultReaderRead","_errorSteps","e","hasOwnProperty","_disturbed","_readableStreamController","defineProperties","cancel","enumerable","read","releaseLock","closed","toStringTag","defineProperty","configurable","asyncIterator","preventCancel","_preventCancel","ReadableStreamAsyncIteratorImpl","nextSteps","_this","_nextSteps","_ongoingPromise","returnSteps","_returnSteps","_isFinished","result","ReadableStreamAsyncIteratorPrototype","next","IsReadableStreamAsyncIterator","_asyncIteratorImpl","streamAsyncIteratorBrandCheckException","return","setPrototypeOf","NumberIsNaN","isNaN","IsFiniteNonNegativeNumber","IsNonNegativeNumber","Infinity","DequeueValue","container","pair","_queue","_queueTotalSize","size","EnqueueValueWithSize","RangeError","ResetQueue","CreateArrayFromList","slice","ReadableStreamBYOBRequest","IsReadableStreamBYOBRequest","byobRequestBrandCheckException","_view","bytesWritten","_associatedReadableByteStreamController","buffer","controller","ReadableByteStreamControllerRespondInternal","ReadableByteStreamControllerRespond","view","ArrayBuffer","isView","byteLength","firstDescriptor","_pendingPullIntos","peek","byteOffset","bytesFilled","ReadableByteStreamControllerRespondWithNewView","respond","respondWithNewView","ReadableByteStreamController","IsReadableByteStreamController","byteStreamControllerBrandCheckException","_byobRequest","Uint8Array","byobRequest","create","request","SetUpReadableStreamBYOBRequest","ReadableByteStreamControllerGetDesiredSize","_closeRequested","state","_controlledReadableByteStream","ReadableByteStreamControllerError","ReadableByteStreamControllerClearAlgorithms","ReadableStreamClose","ReadableByteStreamControllerClose","transferredBuffer","ReadableByteStreamControllerEnqueueChunkToQueue","transferredView","ReadableStreamHasBYOBReader","ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue","ReadableByteStreamControllerCallPullIfNeeded","ReadableByteStreamControllerEnqueue","_cancelAlgorithm","entry","ReadableByteStreamControllerHandleQueueDrain","autoAllocateChunkSize","_autoAllocateChunkSize","bufferE","pullIntoDescriptor","elementSize","viewConstructor","readerType","_started","ReadableStreamGetNumReadIntoRequests","ReadableByteStreamControllerShouldCallPull","_pulling","_pullAgain","_pullAlgorithm","ReadableByteStreamControllerCommitPullIntoDescriptor","filledView","ReadableByteStreamControllerConvertPullIntoDescriptor","readIntoRequest","_readIntoRequests","ReadableStreamFulfillReadIntoRequest","ReadableByteStreamControllerFillPullIntoDescriptorFromQueue","currentAlignedBytes","maxBytesToCopy","min","maxBytesFilled","maxAlignedBytes","totalBytesToCopyRemaining","ready","dest","destOffset","src","srcOffset","n","queue","headOfQueue","bytesToCopy","destStart","set","ReadableByteStreamControllerFillHeadPullIntoDescriptor","ReadableByteStreamControllerInvalidateBYOBRequest","ReadableByteStreamControllerShiftPendingPullInto","ReadableByteStreamControllerRespondInClosedState","remainderSize","end","remainder","ReadableByteStreamControllerRespondInReadableState","descriptor","ReadableByteStreamControllerClearPendingPullIntos","ReadableStreamError","_strategyHWM","SetUpReadableByteStreamControllerFromUnderlyingSource","underlyingByteSource","highWaterMark","startAlgorithm","pullAlgorithm","cancelAlgorithm","start","pull","r","SetUpReadableByteStreamController","ReadableStreamAddReadIntoRequest","IsReadableStreamBYOBReader","close","enqueue","error","desiredSize","ReadableStreamBYOBReader","byobReaderBrandCheckException","constructor","DataView","BYTES_PER_ELEMENT","ctor","emptyView","ReadableByteStreamControllerPullInto","ReadableStreamBYOBReaderRead","ExtractHighWaterMark","strategy","defaultHWM","ExtractSizeAlgorithm","convertQueuingStrategy","init","convertQueuingStrategySize","convertUnderlyingSinkAbortCallback","original","convertUnderlyingSinkCloseCallback","convertUnderlyingSinkStartCallback","convertUnderlyingSinkWriteCallback","assertWritableStream","IsWritableStream","rawUnderlyingSink","rawStrategy","underlyingSink","abort","type","write","convertUnderlyingSink","InitializeWritableStream","sizeAlgorithm","WritableStreamDefaultController","writeAlgorithm","closeAlgorithm","abortAlgorithm","SetUpWritableStreamDefaultController","SetUpWritableStreamDefaultControllerFromUnderlyingSink","WritableStream","streamBrandCheckException","IsWritableStreamLocked","WritableStreamAbort","WritableStreamCloseQueuedOrInFlight","WritableStreamClose","AcquireWritableStreamDefaultWriter","WritableStreamDefaultWriter","_writer","_writableStreamController","_writeRequests","_inFlightWriteRequest","_closeRequest","_inFlightCloseRequest","_pendingAbortRequest","_backpressure","_promise","wasAlreadyErroring","_resolve","_reject","_reason","_wasAlreadyErroring","WritableStreamStartErroring","closeRequest","writer","defaultWriterReadyPromiseResolve","closeSentinel","WritableStreamDefaultControllerAdvanceQueueIfNeeded","WritableStreamDealWithRejection","WritableStreamFinishErroring","WritableStreamDefaultWriterEnsureReadyPromiseRejected","WritableStreamHasOperationMarkedInFlight","storedError","forEach","writeRequest","abortRequest","WritableStreamRejectCloseAndClosedPromiseIfNeeded","defaultWriterClosedPromiseReject","WritableStreamUpdateBackpressure","backpressure","defaultWriterReadyPromiseInitialize","defaultWriterReadyPromiseReset","getWriter","locked","_ownerWritableStream","defaultWriterReadyPromiseInitializeAsResolved","defaultWriterClosedPromiseInitialize","defaultWriterReadyPromiseInitializeAsRejected","defaultWriterClosedPromiseResolve","defaultWriterClosedPromiseInitializeAsRejected","IsWritableStreamDefaultWriter","defaultWriterBrandCheckException","defaultWriterLockException","WritableStreamDefaultControllerGetDesiredSize","WritableStreamDefaultWriterGetDesiredSize","_readyPromise","WritableStreamDefaultWriterAbort","WritableStreamDefaultWriterClose","WritableStreamDefaultWriterRelease","WritableStreamDefaultWriterWrite","WritableStreamDefaultWriterEnsureClosedPromiseRejected","_closedPromiseState","defaultWriterClosedPromiseResetToRejected","_readyPromiseState","defaultWriterReadyPromiseReject","defaultWriterReadyPromiseResetToRejected","releasedError","chunkSize","_strategySizeAlgorithm","chunkSizeE","WritableStreamDefaultControllerErrorIfNeeded","WritableStreamDefaultControllerGetChunkSize","WritableStreamAddWriteRequest","enqueueE","_controlledWritableStream","WritableStreamDefaultControllerGetBackpressure","WritableStreamDefaultControllerWrite","IsWritableStreamDefaultController","WritableStreamDefaultControllerError","_abortAlgorithm","WritableStreamDefaultControllerClearAlgorithms","_writeAlgorithm","_closeAlgorithm","WritableStreamMarkCloseRequestInFlight","sinkClosePromise","WritableStreamFinishInFlightClose","WritableStreamFinishInFlightCloseWithError","WritableStreamDefaultControllerProcessClose","WritableStreamMarkFirstWriteRequestInFlight","WritableStreamFinishInFlightWrite","WritableStreamFinishInFlightWriteWithError","WritableStreamDefaultControllerProcessWrite","_readyPromise_resolve","_readyPromise_reject","NativeDOMException","DOMException","isDOMExceptionConstructor","message","Error","captureStackTrace","writable","ReadableStreamPipeTo","source","preventClose","preventAbort","signal","shuttingDown","currentWrite","action","actions","shutdownWithAction","all","map","aborted","addEventListener","isOrBecomesErrored","shutdown","WritableStreamDefaultWriterCloseWithErrorPropagation","destClosed_1","waitForWritesToFinish","oldCurrentWrite","originalIsError","originalError","doTheRest","finalize","newError","isError","removeEventListener","resolveLoop","rejectLoop","resolveRead","rejectRead","ReadableStreamDefaultController","IsReadableStreamDefaultController","defaultControllerBrandCheckException","ReadableStreamDefaultControllerGetDesiredSize","ReadableStreamDefaultControllerCanCloseOrEnqueue","ReadableStreamDefaultControllerClose","ReadableStreamDefaultControllerEnqueue","ReadableStreamDefaultControllerError","ReadableStreamDefaultControllerClearAlgorithms","_controlledReadableStream","ReadableStreamDefaultControllerCallPullIfNeeded","ReadableStreamDefaultControllerShouldCallPull","SetUpReadableStreamDefaultController","convertUnderlyingSourceCancelCallback","convertUnderlyingSourcePullCallback","convertUnderlyingSourceStartCallback","convertReadableStreamType","convertReadableStreamReaderMode","mode","convertPipeOptions","options","isAbortSignal","assertAbortSignal","Boolean","rawUnderlyingSource","underlyingSource","convertUnderlyingDefaultOrByteSource","InitializeReadableStream","SetUpReadableStreamDefaultControllerFromUnderlyingSource","ReadableStream","rawOptions","convertReaderOptions","rawTransform","transform","readable","convertReadableWritablePair","destination","branches","cloneForBranch2","reason1","reason2","branch1","branch2","resolveCancelPromise","reading","canceled1","canceled2","cancelPromise","value1","value2","CreateReadableStream","compositeReason","cancelResult","ReadableStreamTee","impl","convertIteratorOptions","convertQueuingStrategyInit","getReader","pipeThrough","pipeTo","tee","values","byteLengthSizeFunction","_byteLengthQueuingStrategyHighWaterMark","ByteLengthQueuingStrategy","IsByteLengthQueuingStrategy","byteLengthBrandCheckException","countSizeFunction","_countQueuingStrategyHighWaterMark","CountQueuingStrategy","IsCountQueuingStrategy","countBrandCheckException","convertTransformerFlushCallback","convertTransformerStartCallback","convertTransformerTransformCallback","rawTransformer","rawWritableStrategy","rawReadableStrategy","writableStrategy","readableStrategy","transformer","flush","readableType","writableType","convertTransformer","startPromise_resolve","readableHighWaterMark","readableSizeAlgorithm","writableHighWaterMark","writableSizeAlgorithm","startPromise","_writable","CreateWritableStream","_transformStreamController","_backpressureChangePromise","TransformStreamDefaultControllerPerformTransform","TransformStreamDefaultSinkWriteAlgorithm","_readable","flushPromise","_flushAlgorithm","TransformStreamDefaultControllerClearAlgorithms","TransformStreamError","TransformStreamDefaultSinkCloseAlgorithm","TransformStreamDefaultSinkAbortAlgorithm","TransformStreamSetBackpressure","TransformStreamDefaultSourcePullAlgorithm","TransformStreamErrorWritableAndUnblockWrite","_backpressureChangePromise_resolve","InitializeTransformStream","TransformStreamDefaultController","transformAlgorithm","TransformStreamDefaultControllerEnqueue","transformResultE","flushAlgorithm","_controlledTransformStream","_transformAlgorithm","SetUpTransformStreamDefaultController","SetUpTransformStreamDefaultControllerFromTransformer","TransformStream","IsTransformStream","IsTransformStreamDefaultController","TransformStreamDefaultControllerTerminate","readableController","ReadableStreamDefaultControllerHasBackpressure","terminate","exports","prop"],"mappings":"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"} \ No newline at end of file diff --git a/thirdparty/webmidi.js b/thirdparty/webmidi.js new file mode 100644 index 0000000..f5654ca --- /dev/null +++ b/thirdparty/webmidi.js @@ -0,0 +1,31 @@ +/* + +WebMidi v2.5.1 + +WebMidi.js helps you tame the Web MIDI API. Send and receive MIDI messages with ease. Control instruments with user-friendly functions (playNote, sendPitchBend, etc.). React to MIDI input with simple event listeners (noteon, pitchbend, controlchange, etc.). +https://github.com/djipco/webmidi + + +The MIT License (MIT) + +Copyright (c) 2015-2019, Jean-Philippe Côté + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + + +!function(scope){"use strict";function WebMidi(){if(WebMidi.prototype._singleton)throw new Error("WebMidi is a singleton, it cannot be instantiated directly.");(WebMidi.prototype._singleton=this)._inputs=[],this._outputs=[],this._userHandlers={},this._stateChangeQueue=[],this._processingStateChange=!1,this._midiInterfaceEvents=["connected","disconnected"],this._nrpnBuffer=[[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]],this._nrpnEventsEnabled=!0,this._nrpnTypes=["entry","increment","decrement"],this._notes=["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],this._semitones={C:0,D:2,E:4,F:5,G:7,A:9,B:11},Object.defineProperties(this,{MIDI_SYSTEM_MESSAGES:{value:{sysex:240,timecode:241,songposition:242,songselect:243,tuningrequest:246,sysexend:247,clock:248,start:250,continue:251,stop:252,activesensing:254,reset:255,midimessage:0,unknownsystemmessage:-1},writable:!1,enumerable:!0,configurable:!1},MIDI_CHANNEL_MESSAGES:{value:{noteoff:8,noteon:9,keyaftertouch:10,controlchange:11,channelmode:11,nrpn:11,programchange:12,channelaftertouch:13,pitchbend:14},writable:!1,enumerable:!0,configurable:!1},MIDI_REGISTERED_PARAMETER:{value:{pitchbendrange:[0,0],channelfinetuning:[0,1],channelcoarsetuning:[0,2],tuningprogram:[0,3],tuningbank:[0,4],modulationrange:[0,5],azimuthangle:[61,0],elevationangle:[61,1],gain:[61,2],distanceratio:[61,3],maximumdistance:[61,4],maximumdistancegain:[61,5],referencedistanceratio:[61,6],panspreadangle:[61,7],rollangle:[61,8]},writable:!1,enumerable:!0,configurable:!1},MIDI_CONTROL_CHANGE_MESSAGES:{value:{bankselectcoarse:0,modulationwheelcoarse:1,breathcontrollercoarse:2,footcontrollercoarse:4,portamentotimecoarse:5,dataentrycoarse:6,volumecoarse:7,balancecoarse:8,pancoarse:10,expressioncoarse:11,effectcontrol1coarse:12,effectcontrol2coarse:13,generalpurposeslider1:16,generalpurposeslider2:17,generalpurposeslider3:18,generalpurposeslider4:19,bankselectfine:32,modulationwheelfine:33,breathcontrollerfine:34,footcontrollerfine:36,portamentotimefine:37,dataentryfine:38,volumefine:39,balancefine:40,panfine:42,expressionfine:43,effectcontrol1fine:44,effectcontrol2fine:45,holdpedal:64,portamento:65,sustenutopedal:66,softpedal:67,legatopedal:68,hold2pedal:69,soundvariation:70,resonance:71,soundreleasetime:72,soundattacktime:73,brightness:74,soundcontrol6:75,soundcontrol7:76,soundcontrol8:77,soundcontrol9:78,soundcontrol10:79,generalpurposebutton1:80,generalpurposebutton2:81,generalpurposebutton3:82,generalpurposebutton4:83,reverblevel:91,tremololevel:92,choruslevel:93,celestelevel:94,phaserlevel:95,databuttonincrement:96,databuttondecrement:97,nonregisteredparametercoarse:98,nonregisteredparameterfine:99,registeredparametercoarse:100,registeredparameterfine:101},writable:!1,enumerable:!0,configurable:!1},MIDI_NRPN_MESSAGES:{value:{entrymsb:6,entrylsb:38,increment:96,decrement:97,paramlsb:98,parammsb:99,nullactiveparameter:127},writable:!1,enumerable:!0,configurable:!1},MIDI_CHANNEL_MODE_MESSAGES:{value:{allsoundoff:120,resetallcontrollers:121,localcontrol:122,allnotesoff:123,omnimodeoff:124,omnimodeon:125,monomodeon:126,polymodeon:127},writable:!1,enumerable:!0,configurable:!1},octaveOffset:{value:0,writable:!0,enumerable:!0,configurable:!1}}),Object.defineProperties(this,{supported:{enumerable:!0,get:function(){return"requestMIDIAccess"in navigator}},enabled:{enumerable:!0,get:function(){return void 0!==this.interface}.bind(this)},inputs:{enumerable:!0,get:function(){return this._inputs}.bind(this)},outputs:{enumerable:!0,get:function(){return this._outputs}.bind(this)},sysexEnabled:{enumerable:!0,get:function(){return!(!this.interface||!this.interface.sysexEnabled)}.bind(this)},nrpnEventsEnabled:{enumerable:!0,get:function(){return!!this._nrpnEventsEnabled}.bind(this),set:function(enabled){return this._nrpnEventsEnabled=enabled,this._nrpnEventsEnabled}},nrpnTypes:{enumerable:!0,get:function(){return this._nrpnTypes}.bind(this)},time:{enumerable:!0,get:function(){return performance.now()}}})}var wm=new WebMidi;function Input(midiInput){var that=this;this._userHandlers={channel:{},system:{}},this._midiInput=midiInput,Object.defineProperties(this,{connection:{enumerable:!0,get:function(){return that._midiInput.connection}},id:{enumerable:!0,get:function(){return that._midiInput.id}},manufacturer:{enumerable:!0,get:function(){return that._midiInput.manufacturer}},name:{enumerable:!0,get:function(){return that._midiInput.name}},state:{enumerable:!0,get:function(){return that._midiInput.state}},type:{enumerable:!0,get:function(){return that._midiInput.type}}}),this._initializeUserHandlers(),this._midiInput.onmidimessage=this._onMidiMessage.bind(this)}function Output(midiOutput){var that=this;this._midiOutput=midiOutput,Object.defineProperties(this,{connection:{enumerable:!0,get:function(){return that._midiOutput.connection}},id:{enumerable:!0,get:function(){return that._midiOutput.id}},manufacturer:{enumerable:!0,get:function(){return that._midiOutput.manufacturer}},name:{enumerable:!0,get:function(){return that._midiOutput.name}},state:{enumerable:!0,get:function(){return that._midiOutput.state}},type:{enumerable:!0,get:function(){return that._midiOutput.type}}})}WebMidi.prototype.enable=function(callback,sysex){this.enabled||(this.supported?navigator.requestMIDIAccess({sysex:sysex}).then(function(midiAccess){var promiseTimeout,events=[],promises=[];this.interface=midiAccess,this._resetInterfaceUserHandlers(),this.interface.onstatechange=function(e){events.push(e)};for(var inputs=midiAccess.inputs.values(),input=inputs.next();input&&!input.done;input=inputs.next())promises.push(input.value.open());for(var outputs=midiAccess.outputs.values(),output=outputs.next();output&&!output.done;output=outputs.next())promises.push(output.value.open());function onPortsOpen(){clearTimeout(promiseTimeout),this._updateInputsAndOutputs(),this.interface.onstatechange=this._onInterfaceStateChange.bind(this),"function"==typeof callback&&callback.call(this),events.forEach(function(event){this._onInterfaceStateChange(event)}.bind(this))}promiseTimeout=setTimeout(onPortsOpen.bind(this),200),Promise&&Promise.all(promises).catch(function(err){}).then(onPortsOpen.bind(this))}.bind(this),function(err){"function"==typeof callback&&callback.call(this,err)}.bind(this)):"function"==typeof callback&&callback(new Error("The Web MIDI API is not supported by your browser.")))},WebMidi.prototype.disable=function(){if(!this.supported)throw new Error("The Web MIDI API is not supported by your browser.");this.interface&&(this.interface.onstatechange=void 0),this.interface=void 0,this._inputs=[],this._outputs=[],this._nrpnEventsEnabled=!0,this._resetInterfaceUserHandlers()},WebMidi.prototype.addListener=function(type,listener){if(!this.enabled)throw new Error("WebMidi must be enabled before adding event listeners.");if("function"!=typeof listener)throw new TypeError("The 'listener' parameter must be a function.");if(!(0<=this._midiInterfaceEvents.indexOf(type)))throw new TypeError("The specified event type is not supported.");return this._userHandlers[type].push(listener),this},WebMidi.prototype.hasListener=function(type,listener){if(!this.enabled)throw new Error("WebMidi must be enabled before checking event listeners.");if("function"!=typeof listener)throw new TypeError("The 'listener' parameter must be a function.");if(!(0<=this._midiInterfaceEvents.indexOf(type)))throw new TypeError("The specified event type is not supported.");for(var o=0;o>4,channelBufferIndex=15&e.data[0],channel=1+channelBufferIndex;if(1=wm.MIDI_NRPN_MESSAGES.increment&&data1<=wm.MIDI_NRPN_MESSAGES.parammsb||data1===wm.MIDI_NRPN_MESSAGES.entrymsb||data1===wm.MIDI_NRPN_MESSAGES.entrylsb)){var ccEvent={target:this,type:"controlchange",data:e.data,timestamp:e.timeStamp,channel:channel,controller:{number:data1,name:this.getCcNameByNumber(data1)},value:data2};if(ccEvent.controller.number===wm.MIDI_NRPN_MESSAGES.parammsb&&ccEvent.value!=wm.MIDI_NRPN_MESSAGES.nullactiveparameter)wm._nrpnBuffer[channelBufferIndex]=[],wm._nrpnBuffer[channelBufferIndex][0]=ccEvent;else if(1===wm._nrpnBuffer[channelBufferIndex].length&&ccEvent.controller.number===wm.MIDI_NRPN_MESSAGES.paramlsb)wm._nrpnBuffer[channelBufferIndex].push(ccEvent);else if(2!==wm._nrpnBuffer[channelBufferIndex].length||ccEvent.controller.number!==wm.MIDI_NRPN_MESSAGES.increment&&ccEvent.controller.number!==wm.MIDI_NRPN_MESSAGES.decrement&&ccEvent.controller.number!==wm.MIDI_NRPN_MESSAGES.entrymsb)if(3===wm._nrpnBuffer[channelBufferIndex].length&&wm._nrpnBuffer[channelBufferIndex][2].number===wm.MIDI_NRPN_MESSAGES.entrymsb&&ccEvent.controller.number===wm.MIDI_NRPN_MESSAGES.entrylsb)wm._nrpnBuffer[channelBufferIndex].push(ccEvent);else if(3<=wm._nrpnBuffer[channelBufferIndex].length&&wm._nrpnBuffer[channelBufferIndex].length<=4&&ccEvent.controller.number===wm.MIDI_NRPN_MESSAGES.parammsb&&ccEvent.value===wm.MIDI_NRPN_MESSAGES.nullactiveparameter)wm._nrpnBuffer[channelBufferIndex].push(ccEvent);else if(4<=wm._nrpnBuffer[channelBufferIndex].length&&wm._nrpnBuffer[channelBufferIndex].length<=5&&ccEvent.controller.number===wm.MIDI_NRPN_MESSAGES.paramlsb&&ccEvent.value===wm.MIDI_NRPN_MESSAGES.nullactiveparameter){wm._nrpnBuffer[channelBufferIndex].push(ccEvent);var rawData=[];wm._nrpnBuffer[channelBufferIndex].forEach(function(ev){rawData.push(ev.data)});var nrpnNumber=wm._nrpnBuffer[channelBufferIndex][0].value<<7|wm._nrpnBuffer[channelBufferIndex][1].value,nrpnValue=wm._nrpnBuffer[channelBufferIndex][2].value;6===wm._nrpnBuffer[channelBufferIndex].length&&(nrpnValue=wm._nrpnBuffer[channelBufferIndex][2].value<<7|wm._nrpnBuffer[channelBufferIndex][3].value);var nrpnControllerType="";switch(wm._nrpnBuffer[channelBufferIndex][2].controller.number){case wm.MIDI_NRPN_MESSAGES.entrymsb:nrpnControllerType=wm._nrpnTypes[0];break;case wm.MIDI_NRPN_MESSAGES.increment:nrpnControllerType=wm._nrpnTypes[1];break;case wm.MIDI_NRPN_MESSAGES.decrement:nrpnControllerType=wm._nrpnTypes[2];break;default:throw new Error("The NPRN type was unidentifiable.")}var nrpnEvent={timestamp:ccEvent.timestamp,channel:ccEvent.channel,type:"nrpn",data:rawData,controller:{number:nrpnNumber,type:nrpnControllerType,name:"Non-Registered Parameter "+nrpnNumber},value:nrpnValue};wm._nrpnBuffer[channelBufferIndex]=[],this._userHandlers.channel[nrpnEvent.type]&&this._userHandlers.channel[nrpnEvent.type][nrpnEvent.channel]&&this._userHandlers.channel[nrpnEvent.type][nrpnEvent.channel].forEach(function(callback){callback(nrpnEvent)})}else wm._nrpnBuffer[channelBufferIndex]=[];else wm._nrpnBuffer[channelBufferIndex].push(ccEvent)}},Input.prototype._parseChannelEvent=function(e){var data1,data2,command=e.data[0]>>4,channel=1+(15&e.data[0]);1>7&127,lsb=127&value;return this.send(wm.MIDI_SYSTEM_MESSAGES.songposition,[msb,lsb],this._parseTimeParameter(options.time)),this},Output.prototype.sendSongSelect=function(value,options){if(options=options||{},!(0<=(value=Math.floor(value))&&value<=127))throw new RangeError("The song number must be between 0 and 127.");return this.send(wm.MIDI_SYSTEM_MESSAGES.songselect,[value],this._parseTimeParameter(options.time)),this},Output.prototype.sendTuningRequest=function(options){return options=options||{},this.send(wm.MIDI_SYSTEM_MESSAGES.tuningrequest,void 0,this._parseTimeParameter(options.time)),this},Output.prototype.sendClock=function(options){return options=options||{},this.send(wm.MIDI_SYSTEM_MESSAGES.clock,void 0,this._parseTimeParameter(options.time)),this},Output.prototype.sendStart=function(options){return options=options||{},this.send(wm.MIDI_SYSTEM_MESSAGES.start,void 0,this._parseTimeParameter(options.time)),this},Output.prototype.sendContinue=function(options){return options=options||{},this.send(wm.MIDI_SYSTEM_MESSAGES.continue,void 0,this._parseTimeParameter(options.time)),this},Output.prototype.sendStop=function(options){return options=options||{},this.send(wm.MIDI_SYSTEM_MESSAGES.stop,void 0,this._parseTimeParameter(options.time)),this},Output.prototype.sendActiveSensing=function(options){return options=options||{},this.send(wm.MIDI_SYSTEM_MESSAGES.activesensing,[],this._parseTimeParameter(options.time)),this},Output.prototype.sendReset=function(options){return options=options||{},this.send(wm.MIDI_SYSTEM_MESSAGES.reset,void 0,this._parseTimeParameter(options.time)),this},Output.prototype.stopNote=function(note,channel,options){if("all"===note)return this.sendChannelMode("allnotesoff",0,channel,options);var nVelocity=64;return(options=options||{}).rawVelocity?!isNaN(options.velocity)&&0<=options.velocity&&options.velocity<=127&&(nVelocity=options.velocity):!isNaN(options.velocity)&&0<=options.velocity&&options.velocity<=1&&(nVelocity=127*options.velocity),this._convertNoteToArray(note).forEach(function(item){wm.toMIDIChannels(channel).forEach(function(ch){this.send((wm.MIDI_CHANNEL_MESSAGES.noteoff<<4)+(ch-1),[item,Math.round(nVelocity)],this._parseTimeParameter(options.time))}.bind(this))}.bind(this)),this},Output.prototype.playNote=function(note,channel,options){var time,nVelocity=64;if((options=options||{}).rawVelocity?!isNaN(options.velocity)&&0<=options.velocity&&options.velocity<=127&&(nVelocity=options.velocity):!isNaN(options.velocity)&&0<=options.velocity&&options.velocity<=1&&(nVelocity=127*options.velocity),time=this._parseTimeParameter(options.time),this._convertNoteToArray(note).forEach(function(item){wm.toMIDIChannels(channel).forEach(function(ch){this.send((wm.MIDI_CHANNEL_MESSAGES.noteon<<4)+(ch-1),[item,Math.round(nVelocity)],time)}.bind(this))}.bind(this)),!isNaN(options.duration)){options.duration<=0&&(options.duration=0);var nRelease=64;options.rawVelocity?!isNaN(options.release)&&0<=options.release&&options.release<=127&&(nRelease=options.release):!isNaN(options.release)&&0<=options.release&&options.release<=1&&(nRelease=127*options.release),this._convertNoteToArray(note).forEach(function(item){wm.toMIDIChannels(channel).forEach(function(ch){this.send((wm.MIDI_CHANNEL_MESSAGES.noteoff<<4)+(ch-1),[item,Math.round(nRelease)],(time||wm.time)+options.duration)}.bind(this))}.bind(this))}return this},Output.prototype.sendKeyAftertouch=function(note,channel,pressure,options){var that=this;if(options=options||{},channel<1||16>7&127,lsb=127&fine;return wm.toMIDIChannels(channel).forEach(function(){that.setRegisteredParameter("channelcoarsetuning",coarse,channel,{time:options.time}),that.setRegisteredParameter("channelfinetuning",[msb,lsb],channel,{time:options.time})}),this},Output.prototype.setTuningProgram=function(value,channel,options){var that=this;if(options=options||{},!(0<=(value=Math.floor(value))&&value<=127))throw new RangeError("The program value must be between 0 and 127");return wm.toMIDIChannels(channel).forEach(function(){that.setRegisteredParameter("tuningprogram",value,channel,{time:options.time})}),this},Output.prototype.setTuningBank=function(value,channel,options){var that=this;if(options=options||{},!(0<=(value=Math.floor(value)||0)&&value<=127))throw new RangeError("The bank value must be between 0 and 127");return wm.toMIDIChannels(channel).forEach(function(){that.setRegisteredParameter("tuningbank",value,channel,{time:options.time})}),this},Output.prototype.sendChannelMode=function(command,value,channel,options){if(options=options||{},"string"==typeof command){if(!(command=wm.MIDI_CHANNEL_MODE_MESSAGES[command]))throw new TypeError("Invalid channel mode message name.")}else if(!(120<=(command=Math.floor(command))&&command<=127))throw new RangeError("Channel mode numerical identifiers must be between 120 and 127.");if((value=Math.floor(value)||0)<0||127>7&127,lsb=127&nLevel;return wm.toMIDIChannels(channel).forEach(function(ch){that.send((wm.MIDI_CHANNEL_MESSAGES.pitchbend<<4)+(ch-1),[lsb,msb],that._parseTimeParameter(options.time))}),this},Output.prototype._parseTimeParameter=function(time){var value,parsed=parseFloat(time);return"string"==typeof time&&"+"===time.substring(0,1)?parsed&&0wm.time&&(value=parsed),value},Output.prototype._convertNoteToArray=function(note){var notes=[];return Array.isArray(note)||(note=[note]),note.forEach(function(item){notes.push(wm.guessNoteNumber(item))}),notes},"function"==typeof define&&"object"==typeof define.amd?define([],function(){return wm}):"undefined"!=typeof module&&module.exports?module.exports=wm:scope.WebMidi||(scope.WebMidi=wm)}(this); \ No newline at end of file