/* * Copyright (c) 2026 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. * */ /*jshint esversion: 6 */ async function main() { // main asyncronous thread; mostly initializes the user settings. var delayedStartupFuncs = []; // translation stuff start //// var ConfigSettings = getById("main-js"); let ln_template = null; let altLabelOverride = null; if (urlParams.has("altlabel")) { try { altLabelOverride = urlParams.get("altlabel") || ""; } catch (e) { altLabelOverride = ""; } if (altLabelOverride) { try { altLabelOverride = decodeURIComponent(altLabelOverride); } catch (e) {} altLabelOverride = altLabelOverride.replace(/_/g, " ").trim(); if (altLabelOverride.length === 0) { altLabelOverride = null; } } else { altLabelOverride = null; } } function applyAltLabelOverride(text) { if (!text) { return; } if (translation && translation.innerHTML) { translation.innerHTML["enter-display-name"] = text; } miscTranslations["enter-display-name"] = text; } 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 (urlParams.has("ln") || urlParams.has("language")) { ln_template = urlParams.get("ln") || urlParams.get("language") || null; } else if (session.language) { ln_template = session.language; } else { const storedLanguage = localStorage.getItem("vdo_ninja_language"); if (storedLanguage) { ln_template = storedLanguage; session.language = storedLanguage; } } } catch (e) { errorlog(e); } if (ln_template === null) { // Only show menu if not in auth mode if (!urlParams.has("auth") && !urlParams.has("requireauth")) { getById("mainmenu").style.opacity = 1; } } else if (ln_template !== false) { // checking if manual lanuage override enabled try { log("Lang Template: " + ln_template); await changeLg(ln_template); if (altLabelOverride) { applyAltLabelOverride(altLabelOverride); } // Only show menu if not in auth mode if (!urlParams.has("auth") && !urlParams.has("requireauth")) { //getById("mainmenu").style.opacity = 1; } } catch (error) { errorlog(error); // Only show menu if not in auth mode if (!urlParams.has("auth") && !urlParams.has("requireauth")) { getById("mainmenu").style.opacity = 1; } } } // Initialize authentication if enabled if (window.vdoAuth) { getById("mainmenu").classList.add("hidden2"); getById("header").classList.add("hidden2"); await window.vdoAuth.init(); // Menu visibility is now handled by auth completion if (session.authMode && (session.authToken || session.authSkipped)) { getById("mainmenu").style.opacity = 1; } } if (location.hostname !== "vdo.ninja" && location.hostname !== "backup.vdo.ninja" && location.hostname !== "proxy.vdo.ninja" && location.hostname !== "alt.vdo.ninja" && location.hostname !== "obs.ninja") { errorReport = false; if (location.hostname === "rtc.ninja") { try { if (session.label === false) { document.title = "RTC.Ninja"; } getById("qos").innerHTML = ""; getById("logoname").innerHTML = ""; getById("helpbutton").style.display = "none"; getById("helpbutton").style.opacity = 0; getById("reportbutton").style.display = "none"; getById("reportbutton").style.opacity = 0; getById("dropButton").classList.add("hidden"); getById("container-4").classList.add("hidden"); if (!(urlParams.has("screenshare") || urlParams.has("ss"))) { getById("container-2").classList.add("hidden"); } //getById("mainmenu").style.opacity = 1; getById("mainmenu").style.margin = "30px 0"; getById("translateButton").style.display = "none"; getById("translateButton").style.opacity = 0; // getById("legal").style.display = "none"; // getById("legal").style.opacity = 0; getById("info").style.display = "none"; getById("info").style.opacity = 0; getById("chatBody").innerHTML = ""; } catch (e) {} } else if (session.label === false) { document.title = location.hostname; } try { if (ln_template === false) { if (location.hostname === "china.vdo.ninja") { changeLg("cn").then(() => { if (altLabelOverride) { applyAltLabelOverride(altLabelOverride); } }); } else { changeLg("blank").then(() => { if (altLabelOverride) { applyAltLabelOverride(altLabelOverride); } }); } } if (location.hostname === "china.vdo.ninja") { session.wss = "wss://china.rtc.ninja:8443"; } //getById("mainmenu").style.opacity = 1; getById("qos").innerHTML = ''; getById("logoname").innerHTML = getById("qos").outerHTML; getById("helpbutton").style.display = "none"; getById("reportbutton").style.display = "none"; getById("chatBody").innerHTML = ""; getById("qos").style.color = "#FFF7"; //getById("qos").style.fontSize = "70%"; getById("logoname").style.display = "none"; getById("logoname").style.margin = "0 0 0 5px"; } catch (error) { getById("mainmenu").style.opacity = 1; errorlog(error); } } else { // check if automatic language translation is available getById("mainmenu").style.opacity = 1; if (location.hostname === "alt.vdo.ninja"){ session.wss = "wss://china.rtc.ninja:8443"; } } if (altLabelOverride) { applyAltLabelOverride(altLabelOverride); } //// translation stuff ends //// if (urlParams.has("cleanoutput") || urlParams.has("clean") || urlParams.has("cleanish")) { session.cleanOutput = true; } if (urlParams.has("mutestatus") || urlParams.has("showmutestate") || urlParams.has("showmuted")){ session.showMuteState = true; } if (urlParams.has("unmutestatus") || urlParams.has("showunmutestate") || urlParams.has("showunmuted")){ session.showUnMuteState = true; } if (urlParams.has("cleanviewer") || urlParams.has("cv")) { session.cleanViewer = true; } if (session.cleanOutput || session.cleanViewer) { session.audioMeterGuest = false; } // Track whether we should swap the default tone for the louder knock sample if (typeof session.knockToneEnabled === "undefined") { session.knockToneEnabled = false; } if (urlParams.has("hidehome")) { session.hidehome = true; } hideHomeCheck(); if (window.obsstudio || isMELD) { session.studioSoftware = true; getById("saveRoom").style.display = "none"; // don't let the user save the room if in OBS } if (urlParams.has("previewmode")) { session.switchMode = true; } try { if (sessionStorage.getItem("deleteWhipOnLoad")) { let deleteWhip = sessionStorage.getItem("deleteWhipOnLoad"); let deleteWhipObj = JSON.parse(deleteWhip); if (deleteWhipObj.location) { try { let targetUrl = new URL(deleteWhipObj.location); targetUrl.protocol = window.location.protocol; let xhttp = new XMLHttpRequest(); xhttp.open("DELETE", targetUrl.toString(), true); if (deleteWhipObj.whipOutputToken) { xhttp.setRequestHeader("Authorization", "Bearer " + deleteWhipObj.whipOutputToken); } xhttp.send(); } catch(e) { log(e); } } sessionStorage.removeItem("deleteWhipOnLoad"); } } catch (e) { errorlog(e); } if (urlParams.has("director") || urlParams.has("dir")) { session.director = urlParams.get("director") || urlParams.get("dir") || session.roomid || urlParams.get("roomid") || urlParams.get("r") || urlParams.get("room") || filename || true; session.effect = null; // so the director can see the effects after a page refresh getById("avatarDiv3").classList.remove("hidden"); // lets the director see the avatar option } if (urlParams.has("feedbackbutton") || urlParams.has("fb")) { getById("unmuteSelf").classList.remove("hidden"); // lets the director see the avatar option //session.selfVolume = urlParams.get("fb") || null; session.selfVolume = urlParams.get("feedbackbutton") || urlParams.get("fb") || null; if (session.selfVolume){ getById("unmuteSelf").setAttribute("title", `Hear yourself at ${parseFloat(session.selfVolume)}% volume`); getById("unmuteSelf").setAttribute("alt", `Hear yourself at ${parseFloat(session.selfVolume)}% volume`); } } if (urlParams.has("controls") || urlParams.has("videocontrols")) { session.showControls = true; // show the video control bar if (urlParams.get("controls") === "false") { session.showControls = false; } else if (urlParams.get("controls") === "0") { session.showControls = false; } else if (urlParams.get("controls") === "off") { session.showControls = false; } } if (urlParams.has("forcecontrols")) { session.showControls = 2; function keepControls() { var tmp = document.activeElement; document.querySelectorAll("video").forEach(ele => { ele.focus(); ele.removeAttribute("controls"); ele.setAttribute("controls", ""); }); tmp.focus(); } getById("main").classList.add("forcecontrols"); setInterval(function () { keepControls(); }, 100); } if (urlParams.has("nocontrols")) { session.showControls = false; // show the video control bar } if (!isIFrame && !session.studioSoftware) { if (ChromiumVersion === 65) { // pass, since probably manycam and that's bugged } else if (getStorage("redirect") == "yes") { setStorage("redirect", "", 0); session.sticky = true; } else if (getStorage("settings") != "") { var URLGOTO = getStorage("settings"); if (URLGOTO && URLGOTO.startsWith("https://")) { if (URLGOTO === window.location.href) { // continue, as its already matched } else if (!session.cleanOutput) { window.focus(); document.body.classList.remove("hidden"); session.sticky = await confirmAlt("Would you like to load your previous session?\n\nThis will redirect you to:\n\n" + URLGOTO, true); if (!session.sticky) { setStorage("settings", "", 0); log("deleting cookie as user said no"); } else { var cookieSettings = decodeURI(URLGOTO); setStorage("redirect", "yes", 1); window.location.replace(cookieSettings); } } else { var cookieSettings = decodeURI(URLGOTO); setStorage("redirect", "yes", 1); window.location.replace(cookieSettings); } } } if (urlParams.has("sticky")) { // won't work with iframes. //if (getStorage("permission") == "") { // session.sticky = confirm("Would you allow us to store a cookie to keep your session settings persistent?"); //} else { session.sticky = true; getById("saveRoom").style.display = "none"; //} //if (session.sticky) { setStorage("permission", "yes", 999); setStorage("settings", encodeURI(window.location.href), 90); //} } } else if (isIFrame && !window.obsstudio && urlParams.has("sticky")) { session.sticky = true; getById("saveRoom").style.display = "none"; } if (urlParams.has("safemode")) { session.safemode = true; // load defa } else { session.store = {}; try { loadSettings(); } catch (e) { errorlog(e); } } if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { try { //getById("electronDragZone").style.cursor="grab"; //getById("header").style.height = "max(calc(2% + 20px), 40px)"; // Override window.prompt to use Electron's dialog via the contextBridge if (window.electronApi && window.electronApi.prompt) { window.prompt = function (title, val) { return window.electronApi.prompt({ title, val }); }; } else { warnlog("electronApi prompt function not available"); } const dragZone = document.getElementById("electronDragZone"); if (dragZone) { dragZone.style.display = "block"; dragZone.style.setProperty("-webkit-app-region", "drag"); } const header = document.getElementById("header"); if (header) { const interactiveSelectors = [ "a", "button", "input", "select", "textarea", "[role='button']", "[onclick]" ]; header.querySelectorAll(interactiveSelectors.join(",")).forEach(node => { node.style.setProperty("-webkit-app-region", "no-drag"); if (!node.style.pointerEvents || node.style.pointerEvents === "") { node.style.pointerEvents = "auto"; } }); } } catch (e) { console.error("Error setting up Electron prompt:", e); } } if (window.electronApi && window.electronApi.exposeDoSomethingInWebApp) { window.electronApi.exposeDoSomethingInWebApp(function (fauxEventData) { //alert("WORKS"); //errorlog("WORKS!"); session.remoteInterfaceAPI(fauxEventData); }); if (window.electronApi.updateVersion){ window.electronApi.updateVersion(session.version); } } if (urlParams.has("retrytimeout")) { session.retryTimeout = parseInt(urlParams.get("retrytimeout")) || 5000; if (session.retryTimeout < 5000) { session.retryTimeout = 5000; } } if (urlParams.has("ptz")) { session.ptz = true; } if (urlParams.has("notios")) { iOS = false; iPad = false; } if (urlParams.has("optimize")) { session.optimize = parseInt(urlParams.get("optimize")) || 0; } if (urlParams.has("nosettings") || urlParams.has("ns")) { session.screensharebutton = false; session.showSettings = false; } if (urlParams.has("nomicbutton") || urlParams.has("nmb")) { getById("mutebutton").style.setProperty("display", "none", "important"); } if (urlParams.has("novice")) { // Hiding advanced items document.querySelectorAll(".advanced").forEach(element => { element.classList.add("hide"); }); } if (urlParams.has("userbackgroundimage") || urlParams.has("userbgimage") || urlParams.has("ubgimg")) { // URL or data:base64 image. Becomes local to this viewer only. let defaultMedia = urlParams.get("userbackgroundimage") || urlParams.get("userbgimage") || urlParams.get("ubgimg") || "./media/backgrounds/1.png"; if (defaultMedia) { try { defaultMedia = decodeURIComponent(defaultMedia); } catch (e) {} session.defaultMedia = defaultMedia; try { let fallbackImage = new Image(); fallbackImage.src = defaultMedia; } catch (e) {} } } if (urlParams.has("userforegroundimage") || urlParams.has("overlayimage") || urlParams.has("overlayimg")) { // URL or data:base64 image. Becomes local to this viewer only. let defaultMedia = urlParams.get("userforegroundimage") || urlParams.get("overlayimage") || urlParams.get("overlayimg") || "./media/avatar1.png"; if (defaultMedia) { try { defaultMedia = decodeURIComponent(defaultMedia); } catch (e) {} session.defaultOverlayMedia = defaultMedia; try { let fallbackImage = new Image(); fallbackImage.src = defaultMedia; } catch (e) {} } } if (urlParams.has("avatarimg") || urlParams.has("bgimage") || urlParams.has("bgimg")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case let avatarImg = urlParams.get("avatarimg") || urlParams.get("bgimage") || urlParams.get("bgimg") || "./media/avatar1.png"; if (avatarImg=="0" || avatarImg == "false" || avatarImg == "no"){ if (session.disableBackground!==false){ session.disableBackground = true; } } else if (avatarImg) { try { avatarImg = decodeURIComponent(avatarImg); } catch (e) {} try { let fallbackImage = new Image(); fallbackImage.src = avatarImg; session.style = -1; fallbackImage.onload = function () { document.documentElement.style.setProperty("--video-background-image", 'url("' + avatarImg + '")'); if (session.meterStyle !== 5) { document.documentElement.style.setProperty("--video-background-image-size", "contain"); } session.disableBackground = false; }; } catch (e) {} } else { if (session.disableBackground!==false){ session.disableBackground = true; } } } if (urlParams.has("avatarimg2") || urlParams.has("bgimage2") || urlParams.has("bgimg2")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case let avatarImg2 = urlParams.get("avatarimg2") || urlParams.get("bgimage2") || urlParams.get("bgimg2") || "./media/avatar2.png"; if (avatarImg2) { try { avatarImg2 = decodeURIComponent(avatarImg2); } catch (e) {} try { let fallbackImage2 = new Image(); fallbackImage2.src = avatarImg2; fallbackImage2.onload = function () { document.documentElement.style.setProperty("--video-background-image-talking", 'url("' + avatarImg2 + '")'); if (session.meterStyle !== 5) { document.documentElement.style.setProperty("--video-background-image-size", "contain"); } }; session.audioEffects = true; session.meterStyle = 4; session.style = -1; if (session.showControls === null) { session.showControls = false; } } catch (e) {} } } if (urlParams.has("avatarimg3") || urlParams.has("bgimage3") || urlParams.has("bgimg3")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case let avatarImg3 = urlParams.get("avatarimg3") || urlParams.get("bgimage3") || urlParams.get("bgimg3") || "./media/avatar3.png"; if (avatarImg3) { try { avatarImg3 = decodeURIComponent(avatarImg3); } catch (e) {} try { let fallbackImage3 = new Image(); fallbackImage3.src = avatarImg3; fallbackImage3.onload = function () { document.documentElement.style.setProperty("--video-background-image-screaming", 'url("' + avatarImg3 + '")'); if (session.meterStyle !== 5) { document.documentElement.style.setProperty("--video-background-image-size", "contain"); } }; session.audioEffects = true; session.meterStyle = 4; session.style = -1; if (session.showControls === null) { session.showControls = false; } } catch (e) {} } } if (urlParams.has("background") || urlParams.has("appbg")) { // URL or data:base64 image. Use &chroma if you want to use a color instead of image. let background = urlParams.get("background") || urlParams.get("appbg") || "./media/logo_cropped.png"; if (background) { try { background = decodeURIComponent(background); } catch (e) {} try { background = 'url("' + background + '")'; document.documentElement.style.setProperty("--background-main-image", background); } catch (e) {} } } if (urlParams.has("poster")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case let posterImage = urlParams.get("poster") || "./media/avatar.webp"; if (posterImage) { try { posterImage = decodeURIComponent(posterImage); session.posterImage = posterImage; } catch (e) {} } } if (urlParams.has("hideplaybutton") || urlParams.has("hpb")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case try { document.getElementById("bigPlayButton").classList.add("hidden"); } catch (e) {} } if (urlParams.has("whip") || urlParams.has("whipview")) { session.whipView = urlParams.get("whip") || urlParams.get("whipview") || false; if (session.whipView) { setTimeout(function () { whipClient(); }, 1000); } } if (urlParams.has("noaudiowhipin")){ session.forceNoAudioWhipIn = true; } if (urlParams.has("novideowhipin")){ session.forceNoVideoWhipIn = true; } if (urlParams.has("autoreload")){ let refreshInterval = parseInt(urlParams.get("autoreload")) || 60; if (refreshInterval){ refreshInterval = refreshInterval*60*1000; // minutes to milliseconds setInterval(function(){ session.hangup(true) }, refreshInterval); } } if (urlParams.has("autoreload24")) { let reloadTime = urlParams.get("autoreload24"); // Parse the reload time let [hours, minutes] = reloadTime.split(':').map(Number); if (!isNaN(hours) && !isNaN(minutes) && hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) { let now = new Date(); let reloadDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes); // If the reload time has already passed today, schedule for tomorrow if (reloadDate <= now) { reloadDate.setDate(reloadDate.getDate() + 1); } let timeUntilReload = reloadDate.getTime() - now.getTime(); setTimeout(function() { session.hangup(true); }, timeUntilReload); } else { console.error("Invalid reload time format. Please use HH:MM in 24-hour format."); } } if (urlParams.has("cftoken") || urlParams.has("cft")) { session.whipOutput = urlParams.get("cftoken") || urlParams.get("cft") || false; if (session.whipOutput) { try { session.whipOutput = decodeURIComponent(session.whipOutput); } catch (e) {} if (!session.whipOutputToken) { session.whipOutputToken = session.whipOutput; } session.whipOutput = "https://cloudflare.vdo.ninja/" + session.whipOutput; session.whipOutputUserSet = true; } } if (urlParams.has("endpage")) { session.redirectHangup = urlParams.get("endpage") || false; session.redirectHangupTimer = 3000; if (session.redirectHangup) { try { session.redirectHangup = decodeURIComponent(session.redirectHangup); getById("hangupContainer").innerHTML = "Hang-up complete. Redirecting shortly..."; } catch (e) {} } if (urlParams.has("endpagetimer")) { session.redirectHangupTimer = urlParams.get("endpagetimer") || 0; session.redirectHangupTimer = parseInt(session.redirectHangupTimer) || 0; } } if (urlParams.has("autoend")) { session.autoEnd = urlParams.get("autoend"); if (session.autoEnd) { session.autoEnd = parseInt(session.autoEnd) || 600000; // default 10 minutes if value provided } else { session.autoEnd = 600000; // default 10 minutes if no value } } if (urlParams.has("whepwait") || urlParams.has("whepicewait")) { // I'm going to use this for all whip/whep for the time being. session.whepWait = urlParams.get("whepwait") || urlParams.get("whepicewait") || 2000; // how long we wait for ice candidates to collect; ms. whep out and whep in session.whepWait = parseInt(session.whepWait); if (session.whepWait < 0) { session.whepWait = 0; } } if (urlParams.has("whipwait") || urlParams.has("whipicewait")) { // I'm going to use this for all whip/whep for the time being. session.whipWait = urlParams.get("whipwait") || urlParams.get("whipicewait") || 2000; // how long we wait for ice candidates to collect; ms. whep out and whep in session.whipWait = parseInt(session.whipWait); if (session.whipWait < 0) { session.whipWait = 0; } } else { session.whipWait = 2000; // REMOVE after october 2024. } if (urlParams.has("whipme")){ // WIP getById("container-18").classList.remove("hidden"); } if (urlParams.has("whippush") || urlParams.has("whipout") || urlParams.has("pushwhip")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case session.whipOutput = urlParams.get("whippush") || urlParams.get("whipout") || urlParams.get("pushwhip") || null; if (session.whipOutput) { try { if (session.whipOutput == "twitch") { session.whipOutput = "https://g.webrtc.live-video.net:4443/v2/offer"; query("#publishOutToken input[type='password']").placeholder = "Twitch stream token here"; } else { session.whipOutput = decodeURIComponent(session.whipOutput); if (!session.whipOutput.startsWith("http://") && !session.whipOutput.startsWith("https://")) { session.whipOutput = "https://" + session.whipOutput; } } } catch (e) { errorlog(e); } } else { getById("publishOutURL").classList.remove("hidden"); } if (session.whipOutput) { session.whipOutputUserSet = true; } if (urlParams.has("whippushtoken") || urlParams.has("whipouttoken") || urlParams.has("pushwhiptoken")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case session.whipOutputToken = urlParams.get("whippushtoken") || urlParams.get("whipouttoken") || urlParams.get("pushwhiptoken") || false; if (!session.whipOutputToken) { getById("publishOutToken").classList.remove("hidden"); } } else if (session.whipOutput !== false) { if (!session.whipOutputToken) { getById("publishOutToken").classList.remove("hidden"); } } getById("startPublishingButton").classList.remove("hidden"); } if (urlParams.has("whipoutkeyframe") ){ session.whipOutKeyframe = parseInt(urlParams.get("whipoutkeyframe")) || 0; } if (urlParams.has("whipoutkeyframenewviewer") ){ session.whipOutKeyframeOnNewViewer = true; } if (urlParams.has("svc") || urlParams.has("scalabilitymode")) { // Experiment with this feature here: https://webrtc.github.io/samples/src/content/extensions/svc/ session.scalabilityMode = urlParams.get("svc") || urlParams.get("scalabilitymode") || "L1T3"; if (!scalabilityModes.includes(session.scalabilityMode)) { scalabilityModes.forEach(sca => { if (sca.toLowerCase() === session.scalabilityMode.toLowerCase()) { session.scalabilityMode = sca; log("Corrected the capitalization of the SVC value. just in case thats important"); } }); } } if (urlParams.has("whepplay") || urlParams.has("whep")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case if (urlParams.get("whepplay") || urlParams.has("whep")) { try { session.whepInput = decodeURIComponent(urlParams.get("whepplay") || urlParams.get("whep")); if (session.whepInput) { setTimeout(function () { whepIn(); }, 1000); } } catch (e) { errorlog(e); } } } if (urlParams.has("whepplaytoken") || urlParams.has("wheptoken")) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case if (urlParams.get("whepplaytoken") || urlParams.get("wheptoken")) { try { session.whepInputToken = urlParams.get("whepplaytoken") || urlParams.get("wheptoken"); } catch (e) { errorlog(e); } } } if (urlParams.has("nomouseevents") || urlParams.has("nme")) { session.disableMouseEvents = true; } if (urlParams.has("overlaycontrols")) { session.overlayControls = true; } if (urlParams.has("novideobutton") || urlParams.has("nvb")) { getById("mutevideobutton").style.setProperty("display", "none", "important"); } if (urlParams.has("nospeakerbutton") || urlParams.has("nsb")) { getById("mutespeakerbutton").style.setProperty("display", "none", "important"); } if (urlParams.has("noscale") || urlParams.has("noscaling")) { session.noScaling = true; } if (urlParams.has("pusheffectsdata")) { session.pushEffectsData = true; } if (urlParams.has("pushloudness") || urlParams.has("getloudness")) { // this sets the loudness IFRAME API output, if available. session.pushLoudness = true; } if (urlParams.has("pushfaces") || urlParams.has("getfaces")) { session.grabFaceData = true; setTimeout(function () { // give the app some time to load getFaces(); }, 2000); } if (urlParams.has("notmobile")) { session.mobile = false; } else if (urlParams.has("mobile")) { session.mobile = true; session.audioEffects = false; // disable audio inbound effects also. session.audioMeterGuest = false; } else if (iOS || iPad) { if (SafariVersion && SafariVersion < 17) { getById("oldiOSWarning").classList.remove("hidden"); // update this to 17 at some point. } session.mobile = true; session.audioEffects = false; // disable audio inbound effects also. session.audioMeterGuest = false; window.addEventListener("resize", function () { // Safari is the new IE. if (session.ws) { var msg = {}; msg.requestSceneUpdate = true; session.sendMessage(msg); } if (screen && screen.orientation && screen.orientation.type) { if (screen.orientation.type.includes("portrait")) { document.getElementsByTagName("html")[0].style.height = "100vh"; setTimeout(function () { document.getElementsByTagName("html")[0].style.height = "100%"; }, 1000); } else if (screen.orientation.type.includes("landscape")) { document.getElementsByTagName("html")[0].style.height = "100vh"; setTimeout(function () { document.getElementsByTagName("html")[0].style.height = "100%"; }, 1000); } } else if (window.matchMedia("(orientation: portrait)").matches) { document.getElementsByTagName("html")[0].style.height = "100vh"; setTimeout(function () { document.getElementsByTagName("html")[0].style.height = "100%"; }, 1000); } else if (window.matchMedia("(orientation: landscape)").matches) { document.getElementsByTagName("html")[0].style.height = "100vh"; setTimeout(function () { document.getElementsByTagName("html")[0].style.height = "100%"; }, 1000); } }); if (/CriOS/i.test(navigator.userAgent)) { // if runngin Chrome on iOS if (!session.cleanOutput) { try { navigator.mediaDevices.getUserMedia; } catch (e) { warnUser("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."); } } } } else if (/Android|Pixel|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { // not sure how accurate this is. session.mobile = true; session.audioEffects = false; // disable audio inbound effects also. session.audioMeterGuest = false; } else { log("MAKE DRAGGABLE"); delayedStartupFuncs.push([makeDraggableElement, getById("subControlButtons")]); if (SafariVersion && !ChromiumVersion) { // if desktop Safari, so macOS, give a note saying it sucks getById("SafariWarning").classList.remove("hidden"); } } if (urlParams.has("broadcasttransfer") || urlParams.has("bct")) { log("Broadcast transfer flag set"); session.broadcastTransfer = urlParams.get("broadcasttransfer") || urlParams.get("bct") || null; if (session.broadcastTransfer === "false") { session.broadcastTransfer = false; } else if (session.broadcastTransfer === "0") { session.broadcastTransfer = false; } else if (session.broadcastTransfer === "no") { session.broadcastTransfer = false; } else if (session.broadcastTransfer === "off") { session.broadcastTransfer = false; } else { session.broadcastTransfer = true; } if (transferSettings) { transferSettings.broadcast = session.broadcastTransfer; } } if (urlParams.has("queuetransfer") || urlParams.has("qt")) { log("Broadcast transfer flag set"); session.queueTransfer = urlParams.get("queuetransfer") || urlParams.get("qt") || null; if (session.queueTransfer === "false") { session.queueTransfer = false; } else if (session.queueTransfer === "0") { session.queueTransfer = false; } else if (session.queueTransfer === "no") { session.queueTransfer = false; } else if (session.queueTransfer === "off") { session.queueTransfer = false; } else { session.queueTransfer = true; } if (transferSettings) { transferSettings.queue = session.queueTransfer; } } if (urlParams.has("broadcast") || urlParams.has("bc")) { log("Broadcast flag set"); session.broadcast = urlParams.get("broadcast") || urlParams.get("bc") || null; if (session.broadcast === "false") { session.broadcast = false; } else if (session.broadcast === "0") { session.broadcast = false; } else if (session.broadcast === "no") { session.broadcast = false; } else if (session.broadcast === "off") { session.broadcast = false; } //if ((iOS) || (iPad)) { // session.nopreview = false; //} else { // session.nopreview = true; //} session.minipreview = 2; // full screen if nothing else on screen. session.style = 1; //getById("header").style.display = "none"; //getById("header").style.opacity = 0; session.showList = true; } if (urlParams.has("showlist")) { session.showList = urlParams.get("showlist"); if (session.showList === "false") { session.showList = false; } else if (session.showList === "0") { session.showList = false; } else if (session.showList === "no") { session.showList = false; } else if (session.showList === "off") { session.showList = false; } else { session.showList = true; } } //if (session.showList===true){ // getById("hideusers").classList.add("hidden"); //} if (urlParams.has("meshcast2")) { session.meshcast2 = urlParams.get("meshcast2") || "any"; meshcast2(); } else if (urlParams.has("meshcast") && !urlParams.has("meshcastfailed")) { session.meshcast = urlParams.get("meshcast") || "any"; meshcast(true); } if (urlParams.has("meshcastcode") || urlParams.has("mccode")) { session.meshcastCode = urlParams.get("meshcastcode") || urlParams.get("mccode") || false; } if (urlParams.has("nomeshcast") || urlParams.has("nowhep")) { session.noMeshcast = urlParams.get("nomeshcast") || urlParams.has("nowhep") || true; } if (urlParams.has("chunkcast")) { session.chunkcast = true; } if (urlParams.get("discordwebhook") || urlParams.get("dwh")) { session.discordHook = decodeURIComponent(urlParams.get("discordwebhook") || urlParams.get("dwh")); if (!session.discordHook.startsWith("https://discord.com/api/webhooks/")){ session.discordHook = "https://discord.com/api/webhooks/"+session.discordHook; } } else if (urlParams.get("discordwebhook2") || urlParams.get("dwh2")) { session.discordHook = decodeURIComponent(urlParams.get("discordwebhook2") || urlParams.get("dwh2")); session.discordHookSensitive = true; if (!session.discordHook.startsWith("https://discord.com/api/webhooks/")){ session.discordHook = "https://discord.com/api/webhooks/"+session.discordHook; } } if (urlParams.has("drawing")) { session.allowDrawing = urlParams.get("drawing") || true; } if (urlParams.has("nohistory")) { session.nohistory = true; } else if (urlParams.has("history")){ session.nohistory = false; } else if (isIFrame){ session.nohistory = true; } if (urlParams.has("pagezoom")) { enableFullscreenZoom(); } //if (urlParams.has('callin')){ // awaitInboundCall()(); //} //if (urlParams.has("relaywss")) { // session.relaywss = true; //} if (urlParams.has("fulltalk") && urlParams.get("fulltalk").length == 6) { listenWebsocket(urlParams.get("fulltalk"), false); // talk and hear all } else if (urlParams.has("justtalk") && urlParams.get("justtalk").length == 6) { joinConference(urlParams.get("justtalk")); // just talk if (urlParams.has("hearptsn")) { listenWebsocket(urlParams.get("justtalk")); // hear ptsn only } } else if (urlParams.has("hearptsn") && urlParams.get("hearptsn").length == 6) { listenWebsocket(urlParams.get("hearptsn")); // hear ptsn only if (urlParams.has("justtalk")) { joinConference(urlParams.get("hearptsn")); } } var filename = false; try { if (!session.decrypted) { filename = window.location.pathname.substring(window.location.pathname.lastIndexOf("/") + 1); filename = filename.replace("??", "?"); 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 var tmpHref = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/?" + filename.split("&").slice(1).join("&"); 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) { var tmpHref = window.location.href.substring(0, window.location.href.lastIndexOf("/")); tmpHref = tmpHref + "/?" + filename; filename = false; //warnUser("Please ensure your URL is correctly formatted."); if (!session.nohistory){ window.history.pushState({ path: tmpHref.toString() }, "", tmpHref.toString()); } } } else { filename = filename2.split("&")[0]; if (filename2 != filename) { warnUser("Warning: Please ensure your URL is correctly formatted."); } } } else { filename = false; } log(filename); } } catch (e) { errorlog(e); } var directorLanding = false; if (session.director) { if (session.director === true) { // room not specified. directorLanding = true; } session.meterStyle = 1; session.signalMeter = true; session.batteryMeter = true; } else if (filename === "director") { directorLanding = true; filename = false; session.meterStyle = 1; session.signalMeter = true; session.batteryMeter = true; } if (urlParams.has("updateonslotschange") || urlParams.has("uosc")) { session.updateOnSlotChange = true; } if (urlParams.has("signalmeter")) { session.signalMeter = urlParams.get("signalmeter"); if (session.signalMeter === "false") { session.signalMeter = false; } else if (session.signalMeter === "0") { session.signalMeter = false; } else if (session.signalMeter === "no") { session.signalMeter = false; } else if (session.signalMeter === "off") { session.signalMeter = false; } else { session.signalMeter = true; } } if (urlParams.has("batterymeter")) { session.batteryMeter = urlParams.get("batterymeter"); if (session.batteryMeter === "false") { session.batteryMeter = false; } else if (session.batteryMeter === "0") { session.batteryMeter = false; } else if (session.batteryMeter === "no") { session.batteryMeter = false; } else if (session.batteryMeter === "off") { session.batteryMeter = false; } else { session.batteryMeter = true; } } if (urlParams.has("rooms")) { session.rooms = urlParams .get("rooms") .split(",") .map(function (e) { return sanitizeRoomName(e); }); getById("rooms").classList.remove("hidden"); } if (urlParams.has("leaveorientationflag")) { session.removeOrientationFlag = false; // leave `a=extmap:3 urn:3gpp:video-orientation\r\n` alone } if (urlParams.has("showdirector") || urlParams.has("sd")) { session.showDirector = parseInt(urlParams.get("showdirector")) || parseInt(urlParams.get("sd")) || true; // if 2, video only allowed. True or 1 will be video + audio allowed. // fyi, true is the same as 1 when == is used, so assert(1==true) is true. } if (urlParams.has("bitratecutoff") || urlParams.has("bitcut")) { session.lowBitrateCutoff = parseInt(urlParams.get("bitratecutoff")) || parseInt(urlParams.get("bitcut")) || 300; // low bitrate cut off. session.hiddenSceneViewBitrate = false; } if (urlParams.has("motionswitch") || urlParams.has("motiondetection")) { // switch OBS to this scene when there is motion, and "solo view" this video in the VDO.Ninja auto-mixer, if used session.motionSwitch = parseInt(urlParams.get("motionswitch")) || parseInt(urlParams.get("motiondetection")) || 15; // threshold of motion needed to trigger session.hiddenSceneViewBitrate = false; } if (urlParams.has("motionrecord") || urlParams.has("recordmotion")) { // switch OBS to this scene when there is motion, and "solo view" this video in the VDO.Ninja auto-mixer, if used session.motionRecord = parseInt(urlParams.get("motionrecord")) || parseInt(urlParams.get("recordmotion")) || 15; // threshold of motion needed to trigger session.hiddenSceneViewBitrate = false; } if (urlParams.has("pausepreview") || urlParams.has("dpp")) { try { session.directorViewBitrate = 0; document.querySelector('#controls_blank button[data-action-type="change-quality1"]').classList.add("pressed"); document.querySelector('#controls_blank button[data-action-type="change-quality2"]').classList.remove("pressed"); document.querySelector('#controls_blank button[data-action-type="change-quality3"]').classList.remove("pressed"); } catch (e) { errorlog(e); } } if (urlParams.has("locked")) { session.locked = urlParams.get("locked"); if (session.locked == "portrait" || session.locked == "vertical") { session.locked = 9.0 / 16.0; } else if (session.locked == "landscape") { session.locked = 16.0 / 9.0; } else if (session.locked == "square") { session.locked = 1.0; } else { session.locked = parseFloat(session.locked) || 16 / 9.0; } } if (urlParams.has("lowbitratescene") || urlParams.has("cutscene")) { session.lowBitrateSceneChange = urlParams.get("lowbitratescene") || urlParams.get("cutscene") || "cutscene"; // low bitrate cut off. session.hiddenSceneViewBitrate = false; if (session.lowBitrateCutoff === false) { session.lowBitrateCutoff = 300; } } if (urlParams.has("rotate")) { session.rotate = urlParams.get("rotate") || 90; session.rotate = parseInt(session.rotate); } if (urlParams.has("rotatewindow") || urlParams.has("rotatepage")) { let rotateThis = parseInt(urlParams.get("rotatewindow")) || parseInt(urlParams.get("rotatepage")) || 90; updateForceRotatedCSS(rotateThis); } if (urlParams.has("facing")) { session.facingMode = urlParams.get("facing") || false; } if (session.facingMode) { session.facingMode = session.facingMode.toLowerCase(); if (session.facingMode == "user") { // } else if (session.facingMode == "environment") { // } else if (session.facingMode == "rear") { session.facingMode = "environment"; } else if (session.facingMode == "back") { session.facingMode = "environment"; } else if (session.facingMode == "front") { session.facingMode = "user"; } else { session.facingMode = false; } } // session.facingMode }; // user or environment if (urlParams.has("forcelandscape") || urlParams.has("forcedlandscape") || urlParams.has("fl")) { session.orientation = "landscape"; if (Firefox) { session.fullscreen = true; // windowed mode complicates things in this mode } } else if (urlParams.has("forceportrait") || urlParams.has("forcedportrait") || urlParams.has("fp")) { session.orientation = "portrait"; if (Firefox) { session.fullscreen = true; // windowed mode complicates things in this mode } } if (urlParams.has("forceviewerlandscape")) { session.keepIncomingVideosInLandscape = parseInt(urlParams.get("forceviewerlandscape")) || 270; } if (urlParams.has("forceviewerportrait")) { session.keepIncomingVideosInPortrait = parseInt(urlParams.get("forceviewerportrait")) || 90; } document.addEventListener("fullscreenchange", event => { log("full screen change event"); log(event); // Handle myVideo class for self-preview when using fullscreen button if (session.fullscreenButton) { var videoSource = document.getElementById("videosource") || document.getElementById("previewWebcam"); if (videoSource) { if (document.fullscreenElement) { // Entering fullscreen - store original class and remove myVideo constraint if (!videoSource.dataset.originalClass) { videoSource.dataset.originalClass = videoSource.className; } videoSource.classList.remove("myVideo"); } else { // Exiting fullscreen - restore original class if (videoSource.dataset.originalClass) { videoSource.className = videoSource.dataset.originalClass; delete videoSource.dataset.originalClass; } } } } if (document.getElementById("previewWebcam")) { // Update fullscreen icon even in preview mode if (document.fullscreenElement) { getById("fullscreenPageToggle").classList.remove("la-expand-arrows-alt"); getById("fullscreenPageToggle").classList.add("la-compress-arrows-alt"); } else { getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); } return; } if (session.orientation && session.mobile) { if (document.fullscreenElement) { document.exitFullscreen(); getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); } return; } if (document.fullscreenElement) { getById("fullscreenPageToggle").classList.remove("la-expand-arrows-alt"); getById("fullscreenPageToggle").classList.add("la-compress-arrows-alt"); } else { getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); } updateMixer(); }); if (urlParams.has("fullscreenbutton") || urlParams.has("fsb")) { // just an alternative; might be compoundable if (!(iOS || iPad)) { session.fullscreenButton = true; document.documentElement.style.setProperty("--full-screen-button", "none"); getById("fullscreenPage").classList.remove("hidden"); } } else if (urlParams.has("nofullscreenbutton") || urlParams.has("nofsb")) { // just an alternative; might be compoundable session.nofullwindowbutton = true; } if (urlParams.has("pip2") || urlParams.has("pipall")) { // just an alternative; might be compoundable if (typeof documentPictureInPicture !== "undefined") { getById("PictureInPicturePage").classList.remove("hidden"); } } if (urlParams.has("midi") || urlParams.has("hotkeys")) { session.midiHotkeys = urlParams.get("midi") || urlParams.get("hotkeys") || 1; session.midiHotkeys = parseInt(session.midiHotkeys); } if (urlParams.has("disablehotkeys")) { session.disableHotKeys = true; } if (urlParams.has("nohangupbutton") || urlParams.has("nohub")) { getById("hangupbutton").style.display = "none"; session.hangupbutton = false; } if (urlParams.has("hangupbutton") || urlParams.has("hub") || urlParams.has("humb64")) { session.hangupbutton = true; } if (urlParams.has("hangupmessage") || urlParams.has("hum") || urlParams.has("humb64")) { let htmlmessage = urlParams.get("hangupmessage") || urlParams.get("hum") || urlParams.get("humb64"); if (urlParams.get("humb64")) { try { htmlmessage = atob(htmlmessage); } catch (e) {} } try { htmlmessage = htmlmessage.replace(/(\r\n|\n|\r)/gm, ""); htmlmessage = decodeURIComponent(htmlmessage); } catch (e) { console.error(e); } getById("hangupContainer").innerHTML = htmlmessage; } if (urlParams.has("socialstream")) { session.socialstream = urlParams.get("socialstream") || false; } if (urlParams.has("midioffset")) { session.midiOffset = urlParams.get("midioffset") || 0; session.midiOffset = parseInt(session.midiOffset); } if (urlParams.has("midiremote") || urlParams.has("remotemidi")) { if (session.director !== false) { session.midiRemote = parseInt(urlParams.get("midiremote")) || parseInt(urlParams.get("remotemidi")) || 4; } else { session.midiRemote = parseInt(urlParams.get("midiremote")) || parseInt(urlParams.get("remotemidi")) || 1; } } if (urlParams.has("midipush") || urlParams.has("midiout") || urlParams.has("mo")) { session.midiOut = parseInt(urlParams.get("midipush")) || parseInt(urlParams.get("midiout")) || parseInt(urlParams.get("mo")) || true; } if (urlParams.has("tc") || urlParams.has("timecode") || urlParams.has("showtimecode")) { session.midiTimecode = urlParams.get("tc") || urlParams.get("timecode") || urlParams.get("showtimecode") || true; } if (urlParams.has("nochunkediframestats")) { session.chunkIframe = false; } if (urlParams.has("midiiframe")) { session.midiIframe = true; } if (urlParams.has("midipull") || urlParams.has("midiin") || urlParams.has("midin") || urlParams.has("mi")) { session.midiIn = parseInt(urlParams.get("midipull")) || parseInt(urlParams.get("midiin")) || parseInt(urlParams.get("midin")) || parseInt(urlParams.get("mi")) || true; } if (urlParams.has("mididelay")) { // midi-in delay session.midiDelay = parseInt(urlParams.get("mididelay")) || 1000; // 1 second playout delay? acts as a buffer as well I guess. } if (urlParams.has("midichannel")) { session.midiChannel = parseInt(urlParams.get("midichannel")) || false; } if (session.midiChannel) { session.midiChannel = parseInt(session.midiChannel); if (session.midiChannel > 16) { session.midiChannel = false; } if (session.midiChannel < 1) { session.midiChannel = false; } } if (urlParams.has("mididevice")) { session.midiDevice = parseInt(urlParams.get("mididevice")) || false; } if (urlParams.has("ptt")) { if (urlParams.get("ptt")) { setHotKeyAuto(urlParams.get("ptt")); } else { promptAlt("Select a hotkey", true, false, getById("pptHotKey").value, false, false, true); } } if (directorLanding) { getById("container-1").classList.remove("hidden"); getById("container-1").classList.add("skip-animation"); getById("container-1").classList.remove("pointer"); } else if (session.director) { // if a director, webcam/screenshare/iframe auto-defaults shouldn't work } else if (urlParams.has("webcam") || urlParams.has("wc") || urlParams.has("miconly")) { session.webcamonly = true; session.screensharebutton = false; if (urlParams.has("miconly")) { session.videoDevice = 0; session.miconly = true; miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); getById("container-3").title = getById("add_camera").innerText; getById("videoMenu").style.display = "none"; getById("container-3").classList.add("microphoneBackground"); getById("flipcamerabutton").style.setProperty("display", "none", "important"); getById("mutevideobutton").style.setProperty("display", "none", "important"); getById("videoMenu3").style.setProperty("display", "none", "important"); getById("previewWebcam").classList.add("miconly"); //if (session.consent){ // setTimeout(function(){ // warnUser("⚠ Privacy warning: The director of this room can remotely switch your camera or microphone without permission.", 8000); // }, 1500); //} } } else if (urlParams.has("screenshare") || urlParams.has("ss")) { session.screenshare = true; if (urlParams.get("screenshare") || urlParams.get("ss")) { session.screenshare = urlParams.get("screenshare") || urlParams.get("ss"); } } else if (urlParams.has("fileshare") || urlParams.has("fs")) { getById("container-5").classList.remove("hidden"); getById("container-5").classList.add("skip-animation"); getById("container-5").classList.remove("pointer"); //getById("sharefilebutton").style.display = "flex"; // this might be obsolete? // getById("mediafileshare").classList.remove("hidden"); if (SafariVersion) { getById("safari_warning_fileshare").classList.remove("hidden"); } else if (!Firefox) { getById("chrome_warning_fileshare").classList.remove("hidden"); } } else if (!session.director && (urlParams.has("website") || urlParams.has("iframe"))) { getById("container-6").classList.remove("hidden"); getById("container-6").classList.add("skip-animation"); getById("container-6").classList.remove("pointer"); session.website = urlParams.get("website") || urlParams.get("iframe") || false; if (session.website) { session.website = decodeURI(session.website); delayedStartupFuncs.push([session.publishIFrame, session.website]); } } else if (!session.director && (urlParams.has("framegrab"))) { getById("container-6").classList.remove("hidden"); getById("container-6").classList.add("skip-animation"); getById("container-6").classList.remove("pointer"); session.framegrab = urlParams.get("framegrab") || false; session.framegrabAudio = false; session.framegrabAudioRequested = false; const framegrabAudioParamName = urlParams.has("framegrabaudio") ? "framegrabaudio" : (urlParams.has("audio") ? "audio" : null); session.pendingFramegrabAudioSettings = null; if (framegrabAudioParamName) { session.framegrabAudioRequested = true; const rawValue = urlParams.get(framegrabAudioParamName); if (rawValue === null || rawValue === "") { session.framegrabAudio = true; } else { const normalized = String(rawValue).toLowerCase(); session.framegrabAudio = !["0", "false", "no", "off"].includes(normalized); } if (session.framegrabAudio) { session.pendingFramegrabAudioSettings = { enable: true }; } else { session.pendingFramegrabAudioSettings = { enable: false }; } } delayedStartupFuncs.push([session.publishFrameSource, session.framegrab]); // session.publishFrameSource } else if (urlParams.has("webcam2") || urlParams.has("wc2")) { session.webcamonly = true; session.screensharebutton = false; session.introButton = true; } else if (urlParams.has("screenshare2") || urlParams.has("ss2")) { session.screenshare = true; session.introButton = true; if (urlParams.get("screenshare2") || urlParams.get("ss2")) { session.screenshare = urlParams.get("screenshare2") || urlParams.get("ss2"); } } if (session.director && (urlParams.has("website") || urlParams.has("iframe"))) { getById("container-6").classList.remove("hidden"); getById("container-6").classList.add("skip-animation"); getById("container-6").classList.remove("pointer"); session.website = urlParams.get("website") || urlParams.get("iframe") || false; if (session.website) { session.website = decodeURI(session.website); delayedStartupFuncs.push([shareWebsite, session.website]); } } if (urlParams.has("sstype") || urlParams.has("screensharetype")) { // wha type of screen sharing is used; track replace, iframe, or secondary try session.screenshareType = urlParams.get("sstype") || urlParams.get("screensharetype"); session.screenshareType = parseInt(session.screenshareType) || false; } if (urlParams.has("ssstyle") || urlParams.has("screensharestyle")) { // wha type of screen sharing is used; track replace, iframe, or secondary try session.screenshareStyle = urlParams.get("ssstyle") || urlParams.get("screensharestyle") || 1; session.screenshareStyle = parseInt(session.screenshareStyle) || false; } if (urlParams.has("alignright") || urlParams.has("rightalign")) { let alignValue = urlParams.get("alignright"); if (alignValue === null) { alignValue = urlParams.get("rightalign"); } if (alignValue === null || alignValue === "") { session.alignRight = true; } else { const normalizedAlign = String(alignValue).toLowerCase(); session.alignRight = !["0", "false", "no", "off"].includes(normalizedAlign); } } if (urlParams.has("suppresslocalaudio")) { session.suppressLocalAudioPlayback = true; } if (urlParams.has("prefercurrenttab")) { session.preferCurrentTab = true; } if (urlParams.has("selfbrowsersurface")) { // exclude session.selfBrowserSurface = urlParams.get("selfbrowsersurface") || "exclude"; } if (urlParams.has("surfaceswitching")) { session.surfaceSwitching = urlParams.get("surfaceswitching") || "exclude"; } if (urlParams.has("systemaudio")) { // exclude or exclude session.systemAudio = urlParams.get("systemaudio") || "exclude"; } if (urlParams.has("displaysurface")) { // browser, window, or monitor (which is default selected) session.displaySurface = urlParams.get("displaysurface") || "monitor"; } if (urlParams.has("recordwindow") || urlParams.has("rw")) { // Streamlined scene window recording - captures current tab and records to disk session.recordWindow = true; session.cleanOutput = true; session.preferCurrentTab = true; session.selfBrowserSurface = "include"; session.displaySurface = "browser"; session.suppressLocalAudioPlayback = true; if (urlParams.get("recordwindow") || urlParams.get("rw")) { session.recordWindow = parseInt(urlParams.get("recordwindow") || urlParams.get("rw")) || 6000; // bitrate } } if (urlParams.has("locksize")) { // browser, window, or monitor (which is default selected) session.lockWindowSize = urlParams.get("locksize") || true; } if (urlParams.has("intro") || urlParams.has("ib")) { session.introButton = true; } if (urlParams.has("volumecontrol") || urlParams.has("volumecontrols") || urlParams.has("vc")) { if (!(iOS || iPad)) { session.volumeControl = true; } } if (urlParams.has("autohide")) { session.autohide = true; session.dedicatedControlBarSpace = false; } if (urlParams.has("controlbarspace")) { session.dedicatedControlBarSpace = true; } else if (urlParams.has("nocontrolbarspace")) { session.dedicatedControlBarSpace = false; } if (urlParams.has("hidesolo") || urlParams.has("hs")) { session.hidesololinks = true; } if (urlParams.has("ignorehighlight") || urlParams.has("ih")) { session.ignoreHighlight = true; } if (urlParams.has("mute") || urlParams.has("muted") || urlParams.has("m")) { session.muted = true; } if (urlParams.has("hideguest") || urlParams.has("hidden")) { session.directorVideoMuted = true; } if (urlParams.has("videomute") || urlParams.has("videomuted") || urlParams.has("vm")) { session.videoMutedFlag = true; } if (urlParams.has("pauseinvisible")) { session.pauseInvisible = true; } if (urlParams.has("zoomslider")) { session.zoomSlider = true; } if (urlParams.has("ptzslider") || urlParams.has("ptzcontrol") || urlParams.has("ptzcontrols")) { session.ptzSlider = true; } if (urlParams.get("viewslot")) { session.viewslot = parseInt(urlParams.get("viewslot")) || false; session.accept_layouts = true; session.layout = {}; session.exclusiveLayoutAudio = true; session.hiddenSceneViewBitrate = 0; } else if (urlParams.has("layout")) { if (!urlParams.get("layout")) { session.accept_layouts = true; session.layout = {}; } else { let decodedParam; try { decodedParam = decodeURIComponent(urlParams.get("layout")); } catch (e) { decodedParam = urlParams.get("layout"); } try { session.layout = JSON.parse(decodedParam); } catch (e) { try { const base64Decoded = atob(decodedParam); try { session.layout = JSON.parse(base64Decoded); } catch (e) { session.layout = base64Decoded; } } catch (e) { session.layout = decodedParam; } } if (typeof session.layout === 'object' && session.layout !== null && Object.keys(session.layout).length > 0) { session.updateOnSlotChange = true; } } console.warn("Warning: If using &layout with &broadcast, only the director's video will appear in the custom layout, which is likely not intended."); } if (urlParams.get("updateonslotschange") || urlParams.get("uosc")) { let uosc = urlParams.get("updateonslotschange") || urlParams.get("uosc"); if (["false","0","off"].includes(uosc)){ session.updateOnSlotChange = false; } } if (urlParams.get("exclusivelayoutaudio")) { session.exclusiveLayoutAudio = true; } else if (urlParams.get("inclusivelayoutaudio")) { session.exclusiveLayoutAudio = false; } if (urlParams.has("layouts")) { // an ordered array of layouts, which can be used to switch between using the API layouts action. // ie: ?layouts=[[{"x":0,"y":0,"w":100,"h":100,"slot":0}],[{"x":0,"y":0,"w":100,"h":100,"slot":1}],[{"x":0,"y":0,"w":100,"h":100,"slot":2}],[{"x":0,"y":0,"w":100,"h":100,"slot":3}],[{"x":0,"y":0,"w":50,"h":100,"c":false,"slot":0},{"x":50,"y":0,"w":50,"h":100,"c":false,"slot":1}],[{"x":0,"y":0,"w":100,"h":100,"z":0,"c":false,"slot":1},{"x":70,"y":70,"w":30,"h":30,"z":1,"c":true,"slot":0}],[{"x":0,"y":0,"w":50,"h":50,"c":true,"slot":0},{"x":50,"y":0,"w":50,"h":50,"c":true,"slot":1},{"x":0,"y":50,"w":50,"h":50,"c":true,"slot":2},{"x":50,"y":50,"w":50,"h":50,"c":true,"slot":3}],[{"x":0,"y":16.667,"w":66.667,"h":66.667,"c":true,"slot":0},{"x":66.667,"y":0,"w":33.333,"h":33.333,"c":true,"slot":1},{"x":66.667,"y":33.333,"w":33.333,"h":33.333,"c":true,"slot":2},{"x":66.667,"y":66.667,"w":33.333,"h":33.333,"c":true,"slot":3}]] try { session.layouts = JSON.parse(decodeURIComponent(urlParams.get("layouts"))) || JSON.parse(urlParams.get("layouts")) || {}; } catch (e) { try { session.layouts = JSON.parse(urlParams.get("layouts")) || false; } catch (e) { session.layouts = false; } } } /* if (session.layout && session.layouts && (typeof session.layout !== "object") && parseInt(session.layout) && (session.layout == parseInt(session.layout))){ try { session.layout = session.layouts[session.layout-1]; } catch(e){ session.layout= false; } } */ if (urlParams.has("deaf") || urlParams.has("deafen")) { session.directorSpeakerMuted = true; // false == true in this case. } if (urlParams.has("blind")) { session.directorDisplayMuted = true; // false == true in this case. } if (urlParams.has("blindall")) { session.directorBlindButton = true; // false == true in this case. } if (session.directorBlindButton) { getById("blindAllGuests").classList.remove("hidden"); } if (urlParams.has("dpi") || urlParams.has("dpr") || urlParams.has("sharper") || urlParams.has("sharpen")) { session.devicePixelRatio = urlParams.get("dpi") || urlParams.get("dpr") || 2.0; session.devicePixelRatio = parseFloat(session.devicePixelRatio); } //else if (window.devicePixelRatio && window.devicePixelRatio!==1){ // session.devicePixelRatio = window.devicePixelRatio; // this annoys me to no end. //} if (urlParams.has("speakermute") || urlParams.has("speakermuted") || urlParams.has("mutespeaker") || urlParams.has("sm") || urlParams.has("ms")) { var checkState = urlParams.get("speakermute") || urlParams.get("speakermuted") || urlParams.get("mutespeaker") || urlParams.get("sm") || urlParams.get("ms") || true; if (checkState === "false") { session.speakerMuted = false; } else if (checkState === "0") { session.speakerMuted = false; } else if (checkState === "no") { session.speakerMuted = false; } else if (checkState === "off") { session.speakerMuted = false; } else { session.speakerMuted = true; } session.speakerMuted_default = session.speakerMuted; if (session.speakerMuted) { getById("mutespeakertoggle").className = "las la-volume-mute toggleSize"; //getById("mutespeakerbutton").className="hidden 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") || null; 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("hidden"); } } // Tipping feature - unified &tipsid parameter // &tipsid=xxx → use this overlay token, enable tips, fetch username from API // &tipsid (no value) → show onboarding modal for signup // Legacy: &tip, &tips, &tipid also supported for backwards compatibility if (urlParams.has("tipsid") || urlParams.has("tip") || urlParams.has("tipid")) { var tipsIdValue = urlParams.get("tipsid") || urlParams.get("tip") || urlParams.get("tipid"); session.receiveTips = true; if (tipsIdValue) { session.tipsId = tipsIdValue; } else { // No ID specified - show onboarding modal after page loads setTimeout(function() { if (typeof showTipOnboardingModal === 'function') { showTipOnboardingModal(); } }, 2000); } } // Legacy aliases for showing onboarding if (urlParams.has("receivetips") || urlParams.has("tipping")) { session.receiveTips = true; if (!session.tipsId) { setTimeout(function() { if (typeof showTipOnboardingModal === 'function') { showTipOnboardingModal(); } }, 2000); } } if (urlParams.has("tipserver")) { session.tipServer = urlParams.get("tipserver"); } if (urlParams.has("tipamounts")) { try { session.tipAmounts = urlParams.get("tipamounts").split(",").map(x => parseInt(x)).filter(x => x > 0); } catch (e) { session.tipAmounts = [5, 10, 25, 50, 100]; } } if (urlParams.has("tipcurrency")) { session.tipCurrency = urlParams.get("tipcurrency").toUpperCase(); } // Viewer opt-in to see tip UI (two-way opt-in system) if (urlParams.has("showtips") || urlParams.has("supporttips")) { session.showTips = true; } // QR code size for tip overlay (default 150px, min 100px for scanability) if (urlParams.has("tipqrsize")) { session.tipQRSize = Math.max(parseInt(urlParams.get("tipqrsize")) || 150, 100); } // Disable QR code animation if (urlParams.has("notipqr")) { session.noTipQR = true; } if (urlParams.has("app")) { // midi-in delay session.screenshare = false; getById("container-2").classList.add("hidden"); getById("logoname").classList.add("hidden"); getById("head1a").classList.remove("hidden"); getById("main").classList.add("appmode"); getById("jumptoroomButton").innerText = "Join Room"; if (getStorage("jumptoURL")) { getById("joinbyURL").value = getStorage("jumptoURL"); } } if (session.screenshare !== false) { if (session.introButton) { getById("container-3").className = "column columnfade hidden"; // Hide screen share getById("head1").className = "hidden"; } else { getById("container-3").className = "column columnfade hidden"; // Hide webcam getById("container-2").classList.add("skip-animation"); getById("container-2").classList.remove("pointer"); } } if (urlParams.has("hands") || urlParams.has("hand")) { session.raisehands = urlParams.get("hands") || urlParams.get("hand") || 1; session.raisehands = parseInt(session.raisehands); } if (urlParams.has("portrait") || urlParams.has("916") || urlParams.has("vertical")) { // playback aspect ratio session.aspectRatio = 1; // 9:16 (default of 0 is 16:9) } else if (urlParams.has("square") || urlParams.has("11")) { session.aspectRatio = 2; //1:1 ? } else if (urlParams.has("43")) { session.aspectRatio = 3; //1:1 ? } if (urlParams.has("structure")) { session.structure = true; } if (urlParams.has("aspectratio") || urlParams.has("ar")) { // capture aspect ratio session.forceAspectRatio = urlParams.get("aspectratio") || urlParams.get("ar") || false; if (session.forceAspectRatio) { if (session.forceAspectRatio == "portrait" || session.forceAspectRatio == "vertical") { session.forceAspectRatio = 9.0 / 16.0; } else if (session.forceAspectRatio == "landscape") { session.forceAspectRatio = 16.0 / 9.0; } else if (session.forceAspectRatio == "square") { session.forceAspectRatio = 1.0; } else { session.forceAspectRatio = parseFloat(session.forceAspectRatio) || false; } } } if (urlParams.has("screenshareaspectratio") || urlParams.has("ssar")) { // capture aspect ratio session.forceScreenShareAspectRatio = urlParams.get("screenshareaspectratio") || urlParams.get("ssar") || 16.0 / 9.0; if (session.forceScreenShareAspectRatio) { if (session.forceScreenShareAspectRatio == "portrait" || session.forceScreenShareAspectRatio == "vertical") { session.forceScreenShareAspectRatio = 9.0 / 16.0; } else if (session.forceScreenShareAspectRatio == "landscape") { session.forceScreenShareAspectRatio = 16.0 / 9.0; } else if (session.forceScreenShareAspectRatio == "square") { session.forceScreenShareAspectRatio = 1.0; } else { session.forceScreenShareAspectRatio = parseFloat(session.forceScreenShareAspectRatio) || false; } } } if (urlParams.has("crop")) { var crop = parseFloat(urlParams.get("crop")) || 0; if (crop > 0) { session.forceAspectRatio = 1.7777777778 * (crop / 100); } else if (crop < 0) { session.forceAspectRatio = 1.7777777778 / (crop / 100); } else { session.forceAspectRatio = 1.3333333333; } } if (urlParams.has("cover")) { session.cover = urlParams.get("cover") || true; document.documentElement.style.setProperty("--fit-style", "cover"); document.documentElement.style.setProperty("--myvideo-max-width", "100vw"); document.documentElement.style.setProperty("--myvideo-width", "100vw"); document.documentElement.style.setProperty("--myvideo-height", "100vh"); } else if (urlParams.has("fit")) { // not fully implemented yet. session.cover = true; document.documentElement.style.setProperty("--fit-style", "fit"); document.documentElement.style.setProperty("--myvideo-max-width", "100vw"); document.documentElement.style.setProperty("--myvideo-width", "100vw"); document.documentElement.style.setProperty("--myvideo-height", "100vh"); } if (urlParams.has("record")) { if (!session.cleanOutput) { if (SafariVersion && !MediaRecorder) { if (macOS) { warnUser("Your browser may not support local media recording.\n\nTry Chrome instead if on macOS."); } else { warnUser("Your browser or device may not support local media recording.\n\nSafari sometimes allows the feature to be enabled via its experimental settings."); } } else if (SafariVersion) { if (macOS) { warnUser("It is recommended to use Chrome instead of Safari if doing local media recordings."); } else if (SafariVersion <= 15) { warnUser("Please update your device.\n\nOlder versions of Safari may crash after recording for a few minutes."); } else if (iOS || iPad) { // iOS-specific warning about split recordings warnUser("iOS Recording Notice:\n\n• Recordings will be split into 5-minute segments to prevent crashes\n• Files will download as MP4 format\n• Each segment will download separately\n• Use video editing software to join segments if needed\n\nGoogle Drive uploads (if enabled) will work normally."); } else { warnUser("Local media recordings are an experimental feature on Apple devices.\n\nPlease at least test it out a few times first."); } } } session.recordLocal = urlParams.get("record"); if (session.recordLocal === "false" || session.recordLocal === "off") { session.record = false; session.recordLocal = false; } else if (session.recordLocal != parseInt(session.recordLocal)) { session.recordLocal = session.recordDefault; } else if (session.recordLocal !== null){ session.recordLocal = parseInt(session.recordLocal); } else { session.recordLocal = session.recordDefault; } } if (session.record === false) { getById("recordLocalbutton").classList.add("hidden"); getById("recordLocalScreenbutton").classList.add("hidden"); try { document.querySelectorAll('[data-action-type^="record"]').forEach(ele => { ele.remove(); delete ele; }); document.querySelectorAll('[data-action="Record"]').forEach(ele => { ele.parentNode.remove(); delete ele.parentNode; }); } catch (e) { errorlog(e); } } if (urlParams.has("autorecord")) { session.autorecord = true; if (session.recordLocal === false) { let bitautorec = urlParams.get("autorecord"); if (bitautorec !== "") { session.recordLocal = parseInt(bitautorec); } else { session.recordLocal = session.recordDefault; } } } if (urlParams.has("autorecordlocal")) { session.autorecordlocal = true; if (session.recordLocal === false) { session.recordLocal = urlParams.get("autorecordlocal"); if (session.recordLocal != parseInt(session.recordLocal)) { session.recordLocal = session.recordDefault; } else if (session.recordLocal !== ""){ session.recordLocal = parseInt(session.recordLocal); } else { session.recordLocal = session.recordDefault; } } } if (urlParams.has("autorecordremote")) { session.autorecordremote = true; if (session.recordLocal === false) { session.recordLocal = urlParams.get("autorecordremote"); if (session.recordLocal != parseInt(session.recordLocal)) { session.recordLocal = session.recordDefault; } else if (session.recordLocal !== ""){ session.recordLocal = parseInt(session.recordLocal); } else { session.recordLocal = session.recordDefault; } } } if (urlParams.has("splitrecording")) { // minutes session.recordingInterval = urlParams.get("splitrecording") || 5; // 5 minutes session.recordingInterval = parseInt(session.recordingInterval) || 1; // For Mac: https://gist.github.com/steveseguin/8083172a20ad7c9ebcb449e22fc8fe67 // For Windows: https://gist.github.com/steveseguin/7ca1df1df9ec6042f27ecc8d258e3f30 } else if ((SafariVersion || iOS || iPad) && (urlParams.has("record") || urlParams.has("autorecord"))) { // Auto-enable split recording for Safari to prevent memory issues // iOS/iPad: 5 minutes (tighter memory), desktop Safari: 10 minutes session.recordingInterval = (iOS || iPad) ? 5 : 10; if (!session.cleanOutput) { console.log("Safari detected with recording enabled: Auto-enabling split recording (" + session.recordingInterval + "-minute segments) to prevent memory issues"); } } if (urlParams.has("pcm")) { session.pcm = true; } if (urlParams.has("recordcodec") || urlParams.has("rc")) { session.recordingVideoCodec = urlParams.get("recordcodec") || urlParams.get("rc") || false; } else if (session.recordingVideoCodec===false){ session.recordingVideoCodec = "vp8"; } if (urlParams.has("recordfolder")) { session.GDRIVE_FOLDERNAME = urlParams.get("recordfolder") || ""; } if (urlParams.has("menuoffset")) { getById("subControlButtons").style.bottom = urlParams.get("menuoffset") || "50px"; getById("controlPositioning").style.bottom = urlParams.get("menuoffset") || "50px"; getById("subControlButtons").style.setProperty("position", "absolute", "important"); } if (urlParams.has("bigbutton")) { session.bigmutebutton = true; getById("mutebutton").classList.add("bigbutton"); if (urlParams.get("bigbutton")) { let bigbuttontext = document.createElement("span"); bigbuttontext.innerText = urlParams.get("bigbutton"); bigbuttontext.className = "bigbuttontext"; getById("mutebutton").appendChild(bigbuttontext); } } if (urlParams.get("rows")) { session.rows = urlParams.get("rows"); session.rows = session.rows.split(","); } if (urlParams.has("nosettings")) { session.nosettings = true; getById("settingsbutton").classList.add("hidden"); } if (urlParams.has("publish")) { session.publish = true; getById("publishSettings").style.display = "block"; if (session.recordLocal !== false){ getById("startRecordingButton").classList.remove("hidden"); //session.autorecord = true; //getById("startPublishingButton").classList.add("hidden"); } } if (urlParams.has("obscontrols") || urlParams.has("remoteobs") || urlParams.has("obsremote") || urlParams.has("obs") || urlParams.has("controlobs")) { session.obsControls = urlParams.get("obscontrols") || urlParams.get("remoteobs") || urlParams.get("obsremote") || urlParams.get("obs") || urlParams.get("controlobs"); if (session.obsControls) { // whether to show the button or not; that's it. session.obsControls = session.obsControls.toLowerCase(); } if (session.obsControls == "false") { session.obsControls = false; } else if (session.obsControls == "0") { session.obsControls = false; } else if (session.obsControls == "no") { session.obsControls = false; } else if (session.obsControls == "off") { session.obsControls = false; } else if (session.obsControls == "full") { try { session.dataMode = true; session.obsControls = true; //session.doNotSeed = true; getById("main").classList.add("fullscreenOBSControl"); toggleOBSControls(); } catch(e){errorlog(e);} } else if (session.obsControls) { session.obsControls = session.obsControls.toLowerCase(); } else { session.obsControls = true; } } if (urlParams.has("nopush") || urlParams.has("noseed") || urlParams.has("viewonly") || urlParams.has("viewmode")) { // this is like a scene; Seeding is disabled. Can be used with &showall to show all videos on load session.doNotSeed = true; if (session.scene === false) { session.scene = null; // not a scene, but sorta. false vs null makes a difference here. } session.dataMode = true; // thios will let us connect // session.showall = true; // this can be used to SHOW the videos. (&showall) } if (urlParams.has("scene") || urlParams.has("scn")) { session.scene = urlParams.get("scene") || urlParams.get("scn") || 0; if (typeof session.scene === "string") { session.scene = session.scene.replace(/[\W]+/g, "_"); } else { session.scene = (parseInt(session.scene) || 0) + ""; } } if (urlParams.has("morescenes")) { let moreScenes = urlParams.get("morescenes") || 16; moreScenes = parseInt(moreScenes) || 0; if (moreScenes < 8) { moreScenes = 8; } session.maxScene = moreScenes; let sceneButtonMain = document.querySelector("#controls_blank .sceneButtons button"); if (sceneButtonMain && moreScenes) { var i = 8; while (i < moreScenes) { i++; let sceneButton = sceneButtonMain.cloneNode(true); sceneButton.dataset.scene = i; sceneButton.title = "Add to Scene " + i; sceneButton.innerHTML = "S" + i + ""; document.querySelector("#controls_blank .sceneButtons").appendChild(sceneButton); } } } // lets a guest join a scene on their own ... but // doesn't work if the director or scene isn't already loaded. // requires &openscene on the director to be added if (urlParams.has("joinscene") || urlParams.has("joinscenes")) { // list of scenes to auto join. let sceneValues = urlParams.get("joinscene") || urlParams.get("joinscenes"); if (sceneValues){ sceneValues = sceneValues.split(","); session.requestscenes = sceneValues.map(scene => { scene = scene.trim(); if (typeof scene === "string") { return scene.replace(/[\W]+/g, "_"); } else { return (parseInt(scene) || 0) + ""; } }); } } // .. but requires openscene to be set on the target scene. if (urlParams.has("openscene") || urlParams.has("openscenes")) { session.openscene = true; } if (urlParams.has("solo")) { if (session.scene === false) { session.scene = "0"; } session.solo = true; } if (session.scene !== false) { session.disableWebAudio = true; if (session.audioEffects === null) { session.audioEffects = false; } session.audioMeterGuest = false; } if (session.recordWindow && session.scene !== false) { // Add floating record button for scene window recording var recordBtn = document.createElement("button"); recordBtn.id = "recordWindowButton"; recordBtn.innerHTML = "● Start Recording"; recordBtn.title = "Record this scene to a local video file"; recordBtn.style.cssText = "position:fixed;top:10px;right:10px;z-index:99999;padding:12px 20px;font-size:16px;line-height:1.2;height:46px;box-sizing:border-box;background:#d00;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:bold;box-shadow:0 2px 10px rgba(0,0,0,0.3);transition:opacity 0.3s;"; recordBtn.onclick = async function() { if (this.dataset.recording === "1") { // Stop recording if (session.recordWindowElement && session.recordWindowElement.recording) { recordLocalVideo("stop", false, session.recordWindowElement); } this.innerHTML = "● Start Recording"; this.title = "Record this scene to a local video file"; this.style.background = "#d00"; this.style.opacity = "1"; this.dataset.recording = "0"; } else { // Start recording this.innerHTML = "■ Stop"; this.title = "Stop recording"; this.style.background = "#090"; this.style.opacity = "0.3"; // fade out during recording so it's less visible this.dataset.recording = "1"; var bitrate = (typeof session.recordWindow === "number") ? session.recordWindow : 6000; await recordWindowCapture(bitrate); } }; // Hover to show button clearly during recording recordBtn.onmouseenter = function() { this.style.opacity = "1"; }; recordBtn.onmouseleave = function() { if (this.dataset.recording === "1") this.style.opacity = "0.3"; }; document.body.appendChild(recordBtn); // Add Go Live button (experimental - WHIP publish to Twitch) var liveBtn = document.createElement("button"); liveBtn.id = "goLiveButton"; liveBtn.innerHTML = "📡 Go Live"; liveBtn.title = "Stream to Twitch via WHIP (requires stream key)"; liveBtn.style.cssText = "position:fixed;top:10px;right:200px;z-index:99999;padding:12px 20px;font-size:16px;line-height:1.2;height:46px;box-sizing:border-box;background:#6441a5;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:bold;box-shadow:0 2px 10px rgba(0,0,0,0.3);"; liveBtn.onclick = async function() { if (this.dataset.live === "1") { // Stop streaming if (session.goLivePC) { try { session.goLivePC.close(); } catch(e) {} session.goLivePC = null; } this.innerHTML = "📡 Go Live"; this.title = "Stream to Twitch via WHIP (requires stream key)"; this.style.background = "#6441a5"; this.dataset.live = "0"; return; } // Need a stream to publish if (!session.recordWindowElement || !session.recordWindowElement.srcObject) { alert("Please start recording first to capture the scene, then click Go Live."); return; } var streamKey = prompt("Enter your Twitch Stream Key:"); if (!streamKey || !streamKey.trim()) { return; } streamKey = streamKey.trim(); try { // Create RTCPeerConnection for WHIP var config = session.configuration || { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; var pc = new RTCPeerConnection(config); session.goLivePC = pc; // Add tracks from the captured stream var stream = session.recordWindowElement.srcObject; stream.getTracks().forEach(function(track) { pc.addTransceiver(track, { direction: "sendonly", streams: [stream] }); }); // Create and send offer var offer = await pc.createOffer(); await pc.setLocalDescription(offer); // Wait for ICE gathering await new Promise(function(resolve) { if (pc.iceGatheringState === "complete") { resolve(); } else { pc.onicegatheringstatechange = function() { if (pc.iceGatheringState === "complete") resolve(); }; setTimeout(resolve, 2000); // Fallback timeout } }); // Send to Twitch WHIP endpoint var whipUrl = "https://g.webrtc.live-video.net:4443/v2/offer"; var response = await fetch(whipUrl, { method: "POST", headers: { "Content-Type": "application/sdp", "Authorization": "Bearer " + streamKey }, body: pc.localDescription.sdp }); if (!response.ok) { throw new Error("WHIP request failed: " + response.status); } var answerSdp = await response.text(); await pc.setRemoteDescription({ type: "answer", sdp: answerSdp }); // Success - update button this.innerHTML = "🔴 Stop Live"; this.title = "Stop streaming to Twitch"; this.style.background = "#d00"; this.dataset.live = "1"; // Handle connection close pc.onconnectionstatechange = function() { if (pc.connectionState === "failed" || pc.connectionState === "disconnected") { liveBtn.innerHTML = "📡 Go Live"; liveBtn.title = "Stream to Twitch via WHIP (requires stream key)"; liveBtn.style.background = "#6441a5"; liveBtn.dataset.live = "0"; session.goLivePC = null; } }; } catch(e) { console.error("Go Live failed:", e); alert("Failed to go live: " + e.message); if (session.goLivePC) { session.goLivePC.close(); session.goLivePC = null; } this.innerHTML = "📡 Go Live"; this.title = "Stream to Twitch via WHIP (requires stream key)"; this.style.background = "#6441a5"; this.dataset.live = "0"; } }; document.body.appendChild(liveBtn); } if (urlParams.has("fakeuser")) { log("ICE FILTER ENABLED"); session.fakeUser = true; session.dataMode = true; session.autostart = true; session.novideo = []; session.noaudio = []; session.noiframe = []; session.cleanOutput = true; } if (urlParams.has("retransmit")) { session.retransmit = true; session.dataMode = true; } if (urlParams.has("datamode") || urlParams.has("dataonly")) { // this disables all media in/out. session.dataMode = true; } if (urlParams.has("pseudoguest") || urlParams.has("pseudoscene")) { session.videoDevice = 0; session.audioDevice = 0; getById("mainmenu").classList.add("hidden"); getById("header").classList.add("hidden"); getById("mainmenu").style.display = "none"; getById("header").style.display = "none"; session.showList = false; session.autostart = true; getById("controlButtons").classList.add("hidden"); getById("controlButtons").style.display = "none"; getById("miniTaskBar").classList.add("hidden"); getById("miniTaskBar").style.display = "none"; session.dedicatedControlBarSpace = false; session.pseudoguest = true; } if (session.dataMode) { if (!(session.meshcast || session.whipOutput !== false || session.screenshare)) { session.videoDevice = 0; session.audioDevice = 0; } getById("mainmenu").classList.add("hidden"); //session.autohide = true; //session.autostart = true; //session.novideo = []; //session.noaudio = []; //session.noiframe = []; //session.webcamonly = true; } if (urlParams.has("autoadd")) { // the streams we want to view; if set, but let blank, we will request no streams to watch. session.autoadd = urlParams.get("autoadd") || null; // this value can be comma seperated for multiple streams to pull if (session.autoadd == null) { session.autoadd = false; } if (session.autoadd) { session.autoadd = session.autoadd.split(","); } } if (session.director && urlParams.has("autochannels") || urlParams.has("autochannel")) { // Director-only: auto-assign guests to audio channels var val = urlParams.get("autochannels") || urlParams.get("autochannel");; if (val) { // Parse comma-separated channel numbers, filter valid 1-8 session.autochannels = val.split(",") .map(function(n) { return parseInt(n.trim()); }) .filter(function(n) { return n >= 1 && n <= 8; }); if (session.autochannels.length === 0) { session.autochannels = false; } } else { // &autochannels with no value = default allowed list (skip C4/LFE) session.autochannels = [1, 2, 3, 5, 6, 7, 8]; } } if (session.director && urlParams.has("autochannelmode")) { var mode = urlParams.get("autochannelmode"); if (mode === "roundrobin" || mode === "rr") { session.autochannelmode = "roundrobin"; } else { session.autochannelmode = "leastused"; } } if (urlParams.has("preferchannel") || urlParams.has("pc")) { // Guest's preferred audio channel for auto-assignment var ch = parseInt(urlParams.get("preferchannel") || urlParams.get("pc")); if (ch >= 1 && ch <= 8) { session.preferChannel = ch; } } //if (session.scene!=="1"){ // scene =0 and 1 should load instantly. // session.hiddenSceneViewBitrate = 0; // By default this is ~ 400kbps, but if you have 10 scenes, i don't want to kill things. //} if (urlParams.has("hiddenscenebitrate")) { session.hiddenSceneViewBitrate = parseInt(urlParams.get("hiddenscenebitrate")) || 0; } else if (urlParams.has("layout") && session.scene !== false && !session.viewslot) { session.hiddenSceneViewBitrate = false; } else if (urlParams.has("nohiddensceneoptimization")) { session.hiddenSceneViewBitrate = false; } if (urlParams.has("preloadbitrate")) { session.preloadbitrate = parseInt(urlParams.get("preloadbitrate")) || 0; // 1000 } if (urlParams.has("rampuptime")) { session.rampUpTime = parseInt(urlParams.get("rampuptime")) || 10000; } if (urlParams.has("scenetype") || urlParams.has("type")) { session.sceneType = parseInt(urlParams.get("scenetype")) || parseInt(urlParams.get("type")) || false; } if (urlParams.has("mediasettings")) { session.forceMediaSettings = true; } 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")) { session.closedCaptions = true; } if (urlParams.has("nocclabels") || urlParams.has("nocclabel") || urlParams.has("nocaptionlabels") || urlParams.has("nocaptionlabel")) { session.nocaptionlabels = true; } if (urlParams.has("cccolored") || urlParams.has("cccoloured") || urlParams.has("coloredcc") || urlParams.has("colorcc") || urlParams.has("cccolor")) { session.ccColored = true; } if (urlParams.has("base64css") || urlParams.has("b64css") || urlParams.has("cssbase64") || urlParams.has("cssb64")) { try { var base64Css = urlParams.get("base64css") || urlParams.get("b64css") || urlParams.get("cssbase64") || urlParams.get("cssb64"); try { base64Css = atob(base64Css); // window.btoa(encodeURIComponent("#mainmenu{background-color: pink;}" )); } catch (e) {} try { base64Css = decodeURIComponent(base64Css); // window.btoa(encodeURIComponent("#mainmenu{background-color: pink; ❤" )); } catch (e) {} try { if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ session.iFramesAllowed = false; console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); } else if ((window !== window.top) || session.studioSoftware) { // allowed } else { console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); session.iFramesAllowed = false; } } catch(e){ warnlog(e); } var cssStyleSheet = document.createElement("style"); cssStyleSheet.innerText = base64Css; document.querySelector("head").appendChild(cssStyleSheet); } catch (e) { console.error(e); } } if (urlParams.has("css")) { var cssURL = urlParams.get("css"); try { cssURL = decodeURI(cssURL); } catch(e){ warnlog(e); } log(cssURL); let validURL = false; try { cssUrlObj = new URL(cssURL); validURL = true; } catch(e){ } try { if (validURL){ const cssDomain = cssUrlObj.hostname; try { if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ session.iFramesAllowed = false; console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); } else if ((window.location.hostname === cssDomain) || window.location.hostname.endsWith("."+cssDomain) || (window !== window.top) || session.studioSoftware) { if (window.location.hostname !== cssDomain){ console.warn("Third-party CSS has been injected into the site. Security cannot be ensured."); } } else { console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); session.iFramesAllowed = false; } } catch(e){ warnlog(e); } var cssStylesheet = document.createElement("link"); cssStylesheet.rel = "stylesheet"; cssStylesheet.type = "text/css"; cssStylesheet.media = "screen"; cssStylesheet.href = cssURL; document.getElementsByTagName("head")[0].appendChild(cssStylesheet); cssStylesheet.onload = function () { getById("main").classList.remove("hidden"); log("loaded remote style sheet"); }; cssStylesheet.onerror = function () { getById("main").classList.remove("hidden"); errorlog("REMOTE STYLE SHEET HAD ERROR"); }; } else { try { if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); session.iFramesAllowed = false; } else if ((window !== window.top) || session.studioSoftware) { // allowed } else { console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); session.iFramesAllowed = false; } } catch(e){ warnlog(e); } var cssStylesheet = document.createElement("style"); cssStylesheet.innerHTML = cssURL; document.getElementsByTagName("head")[0].appendChild(cssStylesheet); getById("main").classList.remove("hidden"); } } catch(e){ warnlog(e); } } else { getById("main").classList.remove("hidden"); } if (urlParams.has("avatar")) { var avatar = urlParams.get("avatar") || false; if (avatar && avatar == "default") { session.avatar = document.getElementById("defaultAvatar2"); document.body.appendChild(session.avatar); session.avatar.ready = false; session.avatar.onload = () => { session.avatar.ready = true; getById("noAvatarSelected3").classList.remove("selected"); getById("noAvatarSelected").classList.remove("selected"); getById("defaultAvatar1").classList.add("selected"); getById("defaultAvatar2").classList.add("selected"); updateRenderOutpipe(); session.avatar.classList.add("hidden"); }; session.avatar.onerror = () => { session.avatar.classList.add("hidden"); } if (session.avatar.complete) { session.avatar.ready = true; getById("noAvatarSelected3").classList.remove("selected"); getById("noAvatarSelected").classList.remove("selected"); getById("defaultAvatar1").classList.add("selected"); getById("defaultAvatar2").classList.add("selected"); } } else if (avatar) { try { avatar = decodeURIComponent(avatar); } catch (e) {} session.avatar = getById("defaultAvatar2"); document.body.appendChild(session.avatar); session.avatar.ready = false; session.avatar.onload = () => { session.avatar.ready = true; getById("noAvatarSelected3").classList.remove("selected"); getById("noAvatarSelected").classList.remove("selected"); getById("defaultAvatar1").classList.add("selected"); getById("defaultAvatar2").classList.add("selected"); updateRenderOutpipe(); session.avatar.classList.add("hidden"); }; session.avatar.onerror = () => { session.avatar.classList.add("hidden"); } getById("defaultAvatar1").src = avatar; getById("defaultAvatar2").src = avatar; } else { getById("avatarDiv3").classList.remove("hidden"); getById("avatarDiv").classList.remove("hidden"); } if (session.disableBackground!==false){ session.disableBackground = true; } } if (session.disableBackground){ document.documentElement.style.setProperty("--video-background-image", "unset"); } if (urlParams.has("prompt") || urlParams.has("validate") || urlParams.has("approve")) { session.promptAccess = true; } if (urlParams.has("js")) { // ie: &js=https%3A%2F%2Fvdo.ninja%2Fexamples%2Ftestjs.js try { var jsURL = urlParams.get("js"); try { jsURL = decodeURI(jsURL); } catch(e){ warnlog(e); } log(jsURL); const jsUrlObj = new URL(jsURL); const jsDomain = jsUrlObj.hostname; let allow = false; try { if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ console.error("For security and privacy purposes, Javascript injection using Invite Cam must be consented to."); if (!session.cleanOutput){ allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); } } else if ((window.location.hostname === jsDomain) || window.location.hostname.endsWith("."+jsDomain) || (window !== window.top) || session.studioSoftware) { // same domains, iframes, or OBS can run javascript. allow = true; if (window.location.hostname !== jsDomain){ console.warn("Third-party Javascript has been injected into the code. Security cannot be ensured."); } } else if (!session.cleanOutput){ // to allow flexibility, we will allow it if the user consents allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click cancel.", true); } } catch(e){ allow = true; warnlog(e); } if (allow){ // type="text/javascript" crossorigin="anonymous" let externalJavaascript = document.createElement("script"); externalJavaascript.type = "text/javascript"; externalJavaascript.crossorigin = "anonymous"; externalJavaascript.src = jsURL; externalJavaascript.onerror = function () { warnlog("Third-party Javascript failed to load"); }; externalJavaascript.onload = function () { log("Third-party Javascript loaded"); }; document.head.appendChild(externalJavaascript); } else { console.error("For security/privacy purposes, Javascript injection is now only allowed if used within an IFRAME or if the JS file is hosted on the same domain."); } } catch(e){ errorlog(e); } } if (urlParams.has("base64js") || urlParams.has("b64js") || urlParams.has("jsbase64") || urlParams.has("jsb64")) { try { let allow = false; try { if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ console.error("For security and privacy purposes, Javascript injection using Invite Cam must be consented to."); if (!session.cleanOutput){ allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); } } else if ((window !== window.top) || session.studioSoftware) { // iframes or OBS can run javascript. allow = true; console.warn("Third-party Javascript has been injected into the code. Security cannot be ensured."); } else if (!session.cleanOutput){ // to allow flexibility, we will allow it if the user consents allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); } } catch(e){ warnlog(e); allow = true; } if (allow){ var base64js = urlParams.get("base64js") || urlParams.get("b64js") || urlParams.get("jsbase64") || urlParams.get("jsb64"); base64js = decodeURIComponent(atob(base64js)); // window.btoa(encodeURIComponent("alert('hi')")); // ?jsb64=YWxlcnQoJ2hpJyk7 var externalJavaascript = document.createElement("script"); externalJavaascript.type = "text/javascript"; externalJavaascript.crossorigin = "anonymous"; externalJavaascript.innerHTML = base64js; externalJavaascript.onerror = function () { errorlog("Third-party Javascript failed to load"); }; externalJavaascript.onload = function () { log("Third-party Javascript loaded"); }; document.head.appendChild(externalJavaascript); } else { console.error("For security/privacy purposes, Javascript B64 injection is now only allowed if used within an IFRAME."); } } catch (e) { console.error(e); } } session.sitePassword = session.defaultPassword; if (urlParams.has("password") || urlParams.has("pass") || urlParams.has("pw") || urlParams.has("p") || (session.password===null)) { session.password = urlParams.get("password") || urlParams.get("pass") || urlParams.get("pw") || urlParams.get("p") || null; if (!session.password) { window.focus(); session.password = await promptAlt(getTranslation("enter-password"), true, true); if (session.password) { session.password = session.password.trim(); } } 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 { try { session.password = decodeURIComponent(session.password); // will be re-encoded in a moment. } catch (e) { errorlog(e); } } } else if (urlParams.has("nopassword") || urlParams.has("nopass") || urlParams.has("nopw") || urlParams.has("p0")) { session.password = false; session.defaultPassword = false; } if (session.password) { getById("passwordRoom").value = session.password; session.password = sanitizePassword(session.password); session.defaultPassword = false; getById("addPasswordBasic").style.display = "none"; } if (urlParams.has("salt") && urlParams.get("salt")) { session.salt = urlParams.get("salt"); } if (urlParams.has("showconnections")) { session.showConnections = true; // shows remote guest connections as a stat } 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 (hash_input){ hash_input = hash_input.trim(); // yes. really. } if (session.password === false) { window.focus(); session.password = await promptAlt(getTranslation("enter-password-2"), true, true); session.password = sanitizePassword(session.password); getById("passwordRoom").value = session.password; session.defaultPassword = false; } generateHash(session.password + session.salt, 6) .then(function (hash) { // million to one error. I won't log("hash is " + hash); if (hash.substring(0, hash_input.length) !== hash_input) { // this hash crc check is usually just the first 4 characters, but i'll match based on whatever is provided; // max 6 length for security. 2 could be a good option for better security but more than 6 is too big of a security concern. generateHash(session.password + "obs.ninja", 6) .then(function (hash2) { // million to one error; this is to support a legacy salt used. Depreciated, and will be removed eventually log("hash2 is " + hash2); if (hash2.substring(0, 4) !== hash_input) { // this legacy hash crc checks is always 4 characters session.taintedSession = true; if (!session.cleanOutput) { miniTranslate(getById("request_info_prompt"), "password-incorrect"); //getById("request_info_prompt").innerHTML = getTranslation("password-incorrect"); 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"; getById("mainmenu").style.display = "none"; getById("head1").style.display = "none"; } } else { session.taintedSession = false; session.hash = hash; } }) .catch(errorlog); } else { session.taintedSession = false; session.hash = hash; } }) .catch(errorlog); } 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, "_")); //session.style = 6; if (session.showlabels == "") { session.labelstyle = false; } else { session.labelstyle = session.showlabels; } session.showlabels = true; session.manual = session.manual === null ? false : session.manual; session.windowed = session.windowed === null ? false : session.windowed; } if (urlParams.has("showmeta")){ session.showmeta = true; } 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")) { session.label = urlParams.get("label") || urlParams.get("l") || null; var updateURLAsNeed = true; if (session.label == null || session.label.length == 0) { window.focus(); session.label = await promptAlt(getTranslation("enter-display-name"), true); } else { var updateURLAsNeed = false; try { session.label = decodeURIComponent(session.label); } catch (e) { errorlog(e); } session.label = session.label.replace(/_/g, " "); } 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, false); } else { updateURL("label=" + label, true, false); } } } } else if (urlParams.has("defaultlabel") || urlParams.has("labelsuggestion") || urlParams.has("ls")) { session.label = urlParams.get("defaultlabel") || urlParams.get("labelsuggestion") || urlParams.get("ls") || null; var updateURLAsNeed = true; window.focus(); var label = await promptAlt(getTranslation("enter-display-name"), true); if (label) { session.label = sanitizeLabel(label); // alphanumeric was too strict. } else { session.label = sanitizeLabel(session.label); updateURLAsNeed = false; } document.title = session.label; // what the result is. if (updateURLAsNeed) { var label = encodeURIComponent(session.label); if (urlParams.has("l")) { updateURL("l=" + label, true, false); } else { updateURL("label=" + label, true, false); } } } if (session.label){ pokeIframeAPI("this-label", session.label); } if (urlParams.has("resources")) { session.allowResources = urlParams.get("resources") ? urlParams.get("resources").split(",") : true; } if (urlParams.has("meta")) { session.meta = {}; const metaFields = urlParams.get("meta").split(","); const metaTemplates = { pronouns: { id: "1", label: "Preferred Pronouns", type: "select", options: [ "she/her", "he/him", "they/them", "ze/zir", "she/they", "he/they", "[Custom]" ], placeholder: "Select or enter custom pronouns" }, title: { id: "2", label: "Title/Role", type: "text", placeholder: "Indie Developer" }, twitter: { id: "3", label: "X.com username", type: "url", placeholder: "https://x.com/username" }, instagram: { id: "4", label: "Instagram", type: "url", placeholder: "@username" }, youtube: { id: "5", label: "YouTube Channel", type: "url", placeholder: "https://youtube.com/c/channel" }, twitch: { id: "5", label: "Twitch Channel", type: "url", placeholder: "https://www.twitch.tv/username" }, bio: { id: "6", label: "Short Bio", type: "textarea", placeholder: "Tell us about yourself" }, location: { id: "7", label: "Location", type: "text", placeholder: "Toronto, Canada" }, avatar: { id: "8", label: "Profile Avatar", type: "file", accept: "image/jpeg, image/jpg, image/png, image/webp", placeholder: "Upload an avatar image" }, qr: { id: "9", label: "QR Code", type: "file", accept: "image/jpeg, image/jpg, image/png, image/webp", placeholder: "Upload a QR Code" }, }; // Gather meta field data after display name for (const fieldId of metaFields) { const [templateName, field] = Object.entries(metaTemplates) .find(([_, t]) => t.id === fieldId) || []; if (field) { field.templateName = templateName; const value = await promptAlt(field.label, true, false, false, false, false, false, field); if (value) { session.meta[templateName] = { // Use templateName (qr) instead of field.id (9) type: field.type, label: field.label, templateName, value, id: field.id }; } } } } if (urlParams.has("transparent") || urlParams.has("transparency")) { // sets the window to be transparent - useful for IFRAMES? session.transparent = true; } if (urlParams.has("slider") || urlParams.has("showslider")) { session.showSlider = true; } if (session.transparent) { getById("main").style.backgroundColor = "rgba(0,0,0,0)"; document.documentElement.style.setProperty("--container-color", "#0000"); document.documentElement.style.setProperty("--background-color", "#0000"); document.documentElement.style.setProperty("--regular-margin", "0"); document.documentElement.style.setProperty("--director-margin", "0 25px 0 0"); document.documentElement.style.setProperty("--discord-grey-1a", "#0000"); getById("directorLinksButton").style.color = "black"; getById("main").style.overflow = "hidden"; } 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) { session.stereo = session.stereo.toLowerCase(); } //var supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); //supportedConstraints.channelCount; if (session.stereo === "false") { session.stereo = 0; session.audioInputChannels = 1; } else if (session.stereo === "0") { session.stereo = 0; session.audioInputChannels = 1; } else if (session.stereo === "no") { session.stereo = 0; session.audioInputChannels = 1; } else if (session.stereo === "off") { session.stereo = 0; session.audioInputChannels = 1; } else if (session.stereo === "1") { session.stereo = 1; } else if (session.stereo === "both") { session.stereo = 1; } else if (session.stereo === "3") { session.stereo = 3; } else if (session.stereo === "out") { session.stereo = 3; } else if (session.stereo === "mono") { session.stereo = 3; session.audiobitrate = 128; } else if (session.stereo === "4") { session.stereo = 4; } else if (session.stereo === "multi") { session.stereo = 4; } else if (session.stereo === "8") { session.stereo = 8; } else if (session.stereo === "surround") { session.stereo = 8; } else if (session.stereo === "2") { session.stereo = 2; } else if (session.stereo === "6") { session.stereo = 6; } 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 } getById("whipoutstereo").classList.add("hidden"); } if (urlParams.has("screensharestereo") || urlParams.has("sss") || urlParams.has("ssproaudio")) { // 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("screenshare stereo ENABLED"); session.screenshareStereo = urlParams.get("screensharestereo") || urlParams.get("sss") || urlParams.get("ssproaudio"); if (session.screenshareStereo) { session.screenshareStereo = session.screenshareStereo.toLowerCase(); } if (session.screenshareStereo === "false") { session.screenshareStereo = 0; } else if (session.screenshareStereo === "0") { session.screenshareStereo = 0; } else if (session.screenshareStereo === "no") { session.screenshareStereo = 0; } else if (session.screenshareStereo === "off") { session.screenshareStereo = 0; } else if (session.screenshareStereo === "1") { session.screenshareStereo = 1; } else if (session.screenshareStereo === "both") { session.screenshareStereo = 1; } else if (session.screenshareStereo === "3") { session.screenshareStereo = 3; } else if (session.screenshareStereo === "out") { session.screenshareStereo = 3; } else if (session.screenshareStereo === "mono") { session.screenshareStereo = 3; } else if (session.screenshareStereo === "4") { session.screenshareStereo = 4; } else if (session.screenshareStereo === "multi") { session.screenshareStereo = 4; } else if (session.screenshareStereo === "2") { session.screenshareStereo = 2; } else if (session.screenshareStereo === "in") { session.screenshareStereo = 2; } else { session.screenshareStereo = 5; // guests; no stereo in, no high bitrate in, but otherwise like stereo=1 } } // Deploy your own handshake server for free; see: https://github.com/steveseguin/websocket_server if (urlParams.has("pie")) { // piesocket.com support is to be deprecated after dec/19/21, since piesocket is no longer a free service. session.customWSS = urlParams.get("pie") || true; // If session.customWSS == true, then there is no need to set parameters via URL session.wssSetViaUrl = true; if (session.customWSS && session.customWSS !== true) { session.wss = "wss://free3.piesocket.com/v3/1?api_key=" + session.customWSS; // if URL param is set, it will use the API key. } } if ((Firefox && !session.stereo) || session.stereo === 3) { session.mono = true; // this will set the SDP to mono if firefox } 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) { session.stereo = 3; // stereo out only session.audiobitrate = 128; } else if (session.stereo == 2) { session.stereo = 0; session.audiobitrate = 128; } } 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("channelcount") || urlParams.has("ac") || urlParams.has("inputchannels")) { // if updates to this, see also function toggleMonoStereoMic() session.audioInputChannels = urlParams.get("channelcount") || urlParams.get("ac") || urlParams.get("inputchannels") || 0; session.audioInputChannels = parseInt(session.audioInputChannels); if (!session.audioInputChannels) { session.audioInputChannels = false; } } else if (urlParams.has("monomic")) { session.audioInputChannels = 1; } if (session.stereo === 5 && !session.audioInputChannels) { // allow the guest to set their mic to mono. document.querySelectorAll(".gear_microphone").forEach(ele => { ele.classList.remove("hidden"); }); } if (urlParams.has("echocancellation") || urlParams.has("aec") || urlParams.has("ec")) { session.echoCancellation = urlParams.get("echocancellation") || urlParams.get("aec") || urlParams.get("ec"); if (session.echoCancellation) { session.echoCancellation = session.echoCancellation.toLowerCase(); } if (session.echoCancellation == "false") { session.echoCancellation = false; } else if (session.echoCancellation == "0") { session.echoCancellation = false; } else if (session.echoCancellation == "no") { session.echoCancellation = false; } else if (session.echoCancellation == "off") { session.echoCancellation = false; } else { session.echoCancellation = true; } } if (urlParams.has("autogain") || urlParams.has("ag") || urlParams.has("agc")) { session.autoGainControl = urlParams.get("autogain") || urlParams.get("ag") || urlParams.get("agc"); if (session.autoGainControl) { session.autoGainControl = session.autoGainControl.toLowerCase(); } if (session.autoGainControl == "false") { session.autoGainControl = false; } else if (session.autoGainControl == "0") { session.autoGainControl = false; } else if (session.autoGainControl == "no") { session.autoGainControl = false; } else if (session.autoGainControl == "off") { session.autoGainControl = false; } else { session.autoGainControl = true; } } if (urlParams.has("denoise") || urlParams.has("dn")) { session.noiseSuppression = urlParams.get("denoise") || urlParams.get("dn"); if (session.noiseSuppression) { session.noiseSuppression = session.noiseSuppression.toLowerCase(); } if (session.noiseSuppression == "false") { session.noiseSuppression = false; } else if (session.noiseSuppression == "0") { session.noiseSuppression = false; } else if (session.noiseSuppression == "no") { session.noiseSuppression = false; } else if (session.noiseSuppression == "off") { session.noiseSuppression = false; } else { session.noiseSuppression = true; } } if (urlParams.has("isolation") || urlParams.has("voiceisolation") || urlParams.has("vi")) { session.voiceIsolation = urlParams.get("isolation") || urlParams.get("voiceisolation") || urlParams.get("vi"); if (session.voiceIsolation) { session.voiceIsolation = session.voiceIsolation.toLowerCase(); } if (session.voiceIsolation == "false") { session.voiceIsolation = false; } else if (session.voiceIsolation == "0") { session.voiceIsolation = false; } else if (session.voiceIsolation == "no") { session.voiceIsolation = false; } else if (session.voiceIsolation == "off") { session.voiceIsolation = false; } else { session.voiceIsolation = true; } } if (session.voiceIsolation !== null) { getById("whipoutvoiceisolation").classList.add("hidden"); } if (session.noiseSuppression !== null) { getById("whipoutdenoise").classList.add("hidden"); } if (session.autoGainControl !== null) { // should be the last getById("whipoutautogain").classList.add("hidden"); } if (urlParams.has("screenshareaec") || urlParams.has("ssec") || urlParams.has("ssaec")) { session.screenshareAEC = urlParams.get("screenshareaec") || urlParams.get("ssec") || urlParams.get("ssaec"); if (session.screenshareAEC) { session.screenshareAEC = session.screenshareAEC.toLowerCase(); } if (session.screenshareAEC == "false") { session.screenshareAEC = false; } else if (session.screenshareAEC == "0") { session.screenshareAEC = false; } else if (session.screenshareAEC == "no") { session.screenshareAEC = false; } else if (session.screenshareAEC == "off") { session.screenshareAEC = false; } else { session.screenshareAEC = true; } } if (urlParams.has("screenshareautogain") || urlParams.has("ssag") || urlParams.has("ssagc")) { session.screenshareAutogain = urlParams.get("screenshareautogain") || urlParams.get("ssag") || urlParams.get("ssagc"); if (session.screenshareAutogain) { session.screenshareAutogain = session.screenshareAutogain.toLowerCase(); } if (session.screenshareAutogain == "false") { session.screenshareAutogain = false; } else if (session.screenshareAutogain == "0") { session.screenshareAutogain = false; } else if (session.screenshareAutogain == "no") { session.screenshareAutogain = false; } else if (session.screenshareAutogain == "off") { session.screenshareAutogain = false; } else { session.screenshareAutogain = true; } } if (urlParams.has("screensharedenoise") || urlParams.has("ssdn")) { session.screenshareDenoise = urlParams.get("screensharedenoise") || urlParams.get("ssdn"); if (session.screenshareDenoise) { session.screenshareDenoise = session.screenshareDenoise.toLowerCase(); } if (session.screenshareDenoise == "false") { session.screenshareDenoise = false; } else if (session.screenshareDenoise == "0") { session.screenshareDenoise = false; } else if (session.screenshareDenoise == "no") { session.screenshareDenoise = false; } else if (session.screenshareDenoise == "off") { session.screenshareDenoise = false; } else { session.screenshareDenoise = true; } } 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 (urlParams.has("outboundaudiobitrate") || urlParams.has("oab")) { session.outboundAudioBitrate = parseInt(urlParams.get("outboundaudiobitrate")) || parseInt(urlParams.get("oab")) || false; } if (urlParams.has("outboundvideobitrate") || urlParams.has("outboundbitrate") || urlParams.has("ovb")) { session.outboundVideoBitrate = parseInt(urlParams.get("outboundvideobitrate")) || parseInt(urlParams.get("outboundbitrate")) || parseInt(urlParams.get("ovb")) || false; session.outboundVideoBitrate_userSet = true; } else if (session.outboundVideoBitrate!==false){ session.outboundVideoBitrate_userSet = true; } if (urlParams.has("webp") || urlParams.has("images")) { // deprecicating this. chunked mode will replace it. session.webp = urlParams.get("webp") || urlParams.get("images") || "webp"; } if (urlParams.has("webpquality") || urlParams.has("webpq") || urlParams.has("wq")) { session.webPquality = parseInt(urlParams.get("webpquality")) || parseInt(urlParams.get("webpq")) || parseInt(urlParams.get("wq")) || 4; } 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; } // this is to just prevent abuse } if (iOS || iPad) { session.audiobitrate = false; // iOS devices seem to get distortion with custom audio bitrates. Disable for now. } /* if (urlParams.has('whitebalance') || urlParams.has('temp')){ // Need to be applied after the camera is selected. bleh. not enforcible. remove for now. var temperature = urlParams.get('whitebalance') || urlParams.get('temp'); try{ updateCameraConstraints('colorTemperature', parseFloat(temperature)); } catch (e){errorlog(e);} } */ if (urlParams.has("streamid") || urlParams.has("view") || urlParams.has("v") || 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("V") || urlParams.get("pull") || null; // this value can be comma seperated for multiple streams to pull getById("headphonesDiv2").classList.remove("hidden"); getById("headphonesDiv").classList.remove("hidden"); getById("addPasswordBasic").style.display = "none"; if (session.view == null) { session.view = ""; } /* if (session.view){ if (urlParams.has('include') && urlParams.get('include')){ session.view += ","+urlParams.get('include'); } } */ if (session.scene !== false && session.style === false && session.studioSoftware) { session.style = 1; } } // https://vdo.ninja/?fakeguests=10&room=faketestroom123&scene&border=10&padding=20&rounded // https://vdo.ninja/?fakeusers=10&scene&room=test12342345ff if (urlParams.has("fakeguests") || urlParams.has("fakefeeds") || urlParams.has("fakeusers")) { var total = parseInt(urlParams.get("fakeguests")) || parseInt(urlParams.get("fakefeeds")) || parseInt(urlParams.get("fakeusers")) || 4; session.fakeFeeds = []; log("Creating " + total + " fake feeds"); for (var i = 0; i < total; i++) { let fakeElement = document.createElement("video"); fakeElement.autoplay = true; fakeElement.loop = true; fakeElement.muted = true; fakeElement.src = "./media/fakesteve.webm"; fakeElement.labelText = "Fake Steve\\n@steveseguin\\nhe\/him"; fakeElement.dataset.sid = fakeElement.id = parseInt(Math.random() * 10000000000); session.fakeFeeds.push(fakeElement); } if ((session.view!==false) || session.whipView || session.scene !== false || session.whepInput) { setTimeout(function () { updateMixer(); }, 1000); } } if (urlParams.has("directoronly") || urlParams.has("directorsonly") || urlParams.has("do")) { session.viewDirectorOnly = true; } if (session.view !== false) { session.view_set = session.view.split(","); } if (session.view_set) { session.allowScreen = []; session.allowVideos = []; var i = session.view_set.length; while (i--) { var split = session.view_set[i].split(":s"); if (split.length > 1) { session.allowScreen.push(split[0]); session.view_set.splice(i, 1); if (!(split[0] in session.view_set)) { session.view_set.push(split[0]); } } else if (split[0]) { session.allowVideos.push(split[0]); } else { session.view_set.splice(i, 1); } } } if (urlParams.has("include") && urlParams.get("include")) { urlParams.get("include") .split(",") .forEach(sid => { var sidd = sid.split(":s")[0]; if (sidd && !session.include.includes(sidd)) { session.include.push(sidd); } }); } if (urlParams.get("waitpage")) { session.waitPage = urlParams.get("waitpage"); } if (urlParams.has("waitice")) { session.waitForCandidates = true; } if (urlParams.has("directorview") || urlParams.has("dv")) { session.directorView = true; } if (urlParams.has("graphs")) { session.allowGraphs = true; } if (urlParams.has("ruler") || urlParams.has("grid") || urlParams.has("thirds")) { session.fullscreen = true; if (!session.manual) { session.manual = session.manual === null ? false : session.manual; } session.ruleOfThirds = urlParams.get("ruler") || urlParams.get("grid") || urlParams.get("thirds") || "./media/thirds.svg"; session.ruleOfThirds = decodeURIComponent(session.ruleOfThirds); } if (urlParams.has("smallshare") || urlParams.has("smallscreen")) { session.notifyScreenShare = false; } if (urlParams.has("proxy")) { // routes the wss traffic via an alternative network path. Not session.proxy = true; // only works if session.wss is set to false } else if (location.hostname === "proxy.vdo.ninja") { session.proxy = true; } if (urlParams.has("nopreview") || urlParams.has("np")) { log("preview OFF"); session.nopreview = true; if (iOS || iPad) { session.nopreview = false; session.minipreview = 3; // } } else if (urlParams.has("minipreview") || urlParams.has("mini")) { var mini = urlParams.get("minipreview") || urlParams.get("mini"); // 2 is a valid option. (3 is for iPhone with a hidden preview) if (mini === "0") { mini = false; } else if (mini) { mini = parseInt(mini); } else { mini = 1; } log("preview ON"); session.nopreview = false; session.minipreview = mini; if (session.manual === null) { session.manual = session.manual === null ? false : session.manual; } } else if (urlParams.has("largepreview")) { session.nopreview = false; session.minipreview = false; if (session.manual === null) { session.manual = session.manual === null ? false : session.manual; } } else if (urlParams.has("preview") || urlParams.has("showpreview")) { log("preview ON"); if (session.manual === null) { session.manual = session.manual === null ? false : session.manual; } session.nopreview = false; } if (urlParams.has("minipreviewoffset") || urlParams.has("mpo")) { // 40 would be centered session.leftMiniPreview = urlParams.get("minipreviewoffset") || urlParams.get("mpo") || 0; session.leftMiniPreview = parseInt(session.leftMiniPreview) || 0; if (session.leftMiniPreview < -20) { session.leftMiniPreview = -20; } else if (session.leftMiniPreview > 120) { session.leftMiniPreview = 120; } } if (urlParams.has("obsfix")) { session.obsfix = urlParams.get("obsfix"); if (session.obsfix) { session.obsfix = session.obsfix.toLowerCase(); } if (session.obsfix == "false") { session.obsfix = false; } else if (session.obsfix == "0") { session.obsfix = false; } else if (session.obsfix == "no") { session.obsfix = false; } else if (session.obsfix == "off") { session.obsfix = false; } 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("minroombitrate") || urlParams.has("mrb")) { session.minimumRoomBitrate = urlParams.get("minroombitrate") || urlParams.get("mrb") || false; session.minimumRoomBitrate = parseInt(session.minimumRoomBitrate) || false; } if (urlParams.has("remote") || urlParams.has("rem")) { log("remote ENABLED"); session.remote = urlParams.get("remote") || urlParams.get("rem") || true; } if (urlParams.has("slideshow")) { // stream labs mobile fix ? var ssinterval = parseInt(urlParams.get("slideshow")) || 25; ssinterval = 1000 / ssinterval; session.manual = session.manual === null ? true : session.manual; session.dynamicScale = false; setInterval(function () { try { slideshowHack(); } catch (e) { errorlog(e); } }, ssinterval); } 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; } if (urlParams.has("micdelay") || urlParams.has("delay") || urlParams.has("md")) { log("audio gain ENABLED"); session.micDelay = urlParams.get("micdelay") || urlParams.get("delay") || urlParams.get("md") || 0; session.micDelay = parseInt(session.micDelay) || 0; session.disableWebAudio = false; } if (urlParams.has("tips")) { const guestTips = getById("guestTips"); if (guestTips) { const rawTipList = urlParams.get("tips"); const hasTipList = rawTipList && rawTipList.trim().length > 0; const tipItems = guestTips.querySelectorAll(".guest-tip-item"); if (hasTipList) { const selectedTips = new Set( rawTipList .split(",") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0) ); tipItems.forEach((item) => { const tipId = item.dataset.tipId; item.style.display = selectedTips.has(tipId) ? "flex" : "none"; }); } else { tipItems.forEach((item) => { item.style.display = item.dataset.tipDefault === "true" ? "flex" : "none"; }); } guestTips.style.display = "flex"; } } if (urlParams.has("audiogain") || urlParams.has("gain") || urlParams.has("g") || urlParams.has("muteguest")) { log("audio gain ENABLED"); session.audioGain = urlParams.get("audiogain") || urlParams.get("gain") || urlParams.get("g") || 0; session.audioGain = parseInt(session.audioGain) || 0; session.disableWebAudio = false; } if (urlParams.has("volume") || urlParams.has("vol")) { // This sets the default volume for all new video playback elements; 0 to 100. log("setting default volume for playback"); session.volume = urlParams.get("volume") || urlParams.get("vol") || 100; session.volume = parseInt(session.volume) || 0; session.volume = session.volume / 100; // 0 to 1.0 } if (urlParams.has("compressor") || urlParams.has("comp")) { log("audio gain ENABLED"); session.compressor = 1; 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.equalizer = true; session.disableWebAudio = false; } // Mic panning (publisher-side): downmix to mono, then pan to stereo output. if (urlParams.has("micpanning") || urlParams.has("mpan")) { let mp = urlParams.get("micpanning") || urlParams.get("mpan"); if (mp === null || mp === "" || mp === "true") { session.micPanning = 90; // center by default, but enable control } else { mp = parseInt(mp); if (isNaN(mp)) { mp = 90; } if (mp < 0) { mp = 0; } if (mp > 180) { mp = 180; } session.micPanning = mp; } // Ensure WebAudio outbound pipeline is enabled (unless &noap set later) 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("pip")) { session.pip = true; // togglePip //session.manual=true; //innerHTML = } if (urlParams.has("pip3") || urlParams.has("mypip") || urlParams.has("pipme")) { session.pip3 = true; } if (urlParams.has("manual")) { session.manual = true; } 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("obsoff") || urlParams.has("oo") || urlParams.has("disableobs")) { log("OBS feedback disabled"); session.disableOBS = true; getById("obsState").style.setProperty("display", "none", "important"); } if (urlParams.has("hidecodirectors") || urlParams.has("hidecodirector") || urlParams.has("hidedirector") || urlParams.has("hidedirectors") || urlParams.has("hd")) { document.querySelector(":root").style.setProperty("--show-codirectors", "none", "important"); session.hideDirector = true; } if (urlParams.has("pptcontrols") || urlParams.has("slides") || urlParams.has("ppt") || urlParams.has("powerpoint")) { session.pptControls = true; // shows powerpoint controls to remotely control a powerpoint slide. Requires additional remote setup. } if (urlParams.has("allowedscenes")) { session.filterOBSscenes = urlParams.get("allowedscenes"); if (session.filterOBSscenes) { session.filterOBSscenes = session.filterOBSscenes.split(","); } else { session.filterOBSscenes = true; } } if (urlParams.has("tallyoff") || urlParams.has("notally") || urlParams.has("disabletally") || urlParams.has("to")) { log("Tally Light off"); getById("obsState").style.setProperty("display", "none", "important"); } else if (urlParams.has("tally")) { session.tallyStyle = 1; session.tallyStyleDefault = 1; getById("obsState").classList.add("larger"); } if (urlParams.has("automute") || urlParams.has("am")) { session.automute = urlParams.get("automute") || true; session.micIsolatedAutoMute = []; // default auto mutes } if (urlParams.has("noobsstream")){ session.obsState.streaming = false; } if (urlParams.has("noobsvirtual")){ session.obsState.virtualcam = false; } if (urlParams.has("noobsrecord")){ session.obsState.recording = false; } if (urlParams.has("noobssourceactive")){ session.obsState.sourceActive = false; } if (urlParams.has("noobsvisibility")){ session.obsState.visibility = false; } if (window.obsstudio) { session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? session.audioMeterGuest = false; getById("miniTaskBar").classList.add("hidden"); if (session.audioEffects === null) { session.audioEffects = false; } if (window.obsstudio.pluginVersion) { if (macOS) { // if mac, no fix //session.obsfix = false; } else if (window.obsstudio.pluginVersion == "2.17.4") { // if obs v27.2 beta, no fix //session.obsfix = false; } else { var ver = window.obsstudio.pluginVersion.split("."); if (ver.length >= 2) { if (parseInt(ver[0]) <= 2) { if (parseInt(ver[0]) == 2) { if (parseInt(ver[1]) <= 16) { session.obsfix = 15; } } else { session.obsfix = 15; } } } } } try { log("OBS VERSION:" + window.obsstudio.pluginVersion); log("macOS: " + macOS); log(window.obsstudio); if (!urlParams.has("streamlabs")) { var ver1 = window.obsstudio.pluginVersion.split("."); if (ver1.length == 3) { // Should be 3, but disabled3 if (ver1.length == 3 && parseInt(ver1[0]) == 2 && ChromiumVersion < 76 && macOS) { updateURL("streamlabs"); getById("main").innerHTML = "

Update OBS Studio to v26.1.2 or newer; older versions and StreamLabs OBS are not supported on macOS.\
download here: https://github.com/obsproject/obs-studio/releases\



\

Please use the Electron Capture app if there are further problems or if you wish to use StreamLabs on macOS still.

\
You can bypass this error message by refreshing, Clicking Here, or by adding &streamlabs to the URL, but it may still not actually work.\ \
Please report this problem to steve@seguin.email if you feel it is an error.\
"; } } } //if (navigator.userAgent.indexOf('Mac OS X') != -1) { // session.codec = "h264"; // default the codec to h264 if OBS is on macOS (that's all it supports with hardware) // oct 2021, OBS now supports vp8 and actually breaks with h264 android devices. //} if (session.disableOBS === false) { getOBSDetails(); window.addEventListener("obsSceneChanged", obsSceneChanged); if (session.obsState.visibility!==false){ window.addEventListener("obsSourceVisibleChanged", obsSourceVisibleChanged); if (typeof document.visibilityState !== "undefined") { session.obsState.visibility = document.visibilityState === "visible"; } } if (session.obsState.sourceActive!==false){ window.addEventListener("obsSourceActiveChanged", obsSourceActiveChanged); } if (session.obsState.streaming!==false){ window.addEventListener("obsStreamingStarted", obsStreamingStarted); window.addEventListener("obsStreamingStopped", obsStreamingStopped); } if (!session.obsState.recording!==false){ window.addEventListener("obsRecordingStarted", obsRecordingStarted); window.addEventListener("obsRecordingStopped", obsRecordingStopped); } if (session.obsState.virtualcam!==false){ window.addEventListener("obsVirtualcamStarted", obsVirtualcamStarted); window.addEventListener("obsVirtualcamStopped", obsVirtualcamStopped); } } } catch (e) { errorlog(e); } } else if (session.studioSoftware){ session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? session.audioMeterGuest = false; getById("miniTaskBar").classList.add("hidden"); if (session.audioEffects === null) { session.audioEffects = false; } } if (urlParams.has("chroma")) { log("Chroma ENABLED"); getById("main").style.backgroundColor = "#" + (urlParams.get("chroma") || "0F0"); } if (urlParams.has("margin")) { try { session.videoMargin = urlParams.get("margin") || 10; session.videoMargin = parseInt(session.videoMargin); //document.querySelector(':root').style.setProperty('--video-margin', session.videoMargin+"px"); } catch (e) { errorlog("variable css failed"); } } if (urlParams.has("rounded") || urlParams.has("round")) { try { session.borderRadius = urlParams.get("rounded") || urlParams.get("round") || 50; session.borderRadius = parseInt(session.borderRadius); document.querySelector(":root").style.setProperty("--video-rounded", session.borderRadius + "px"); session.manual = session.manual === null ? false : session.manual; session.windowed = session.windowed === null ? false : session.windowed; } catch (e) { errorlog("variable css failed"); } } if (urlParams.has("border")) { try { var videoBorder = urlParams.get("border") || 10; videoBorder = parseInt(videoBorder); session.border = videoBorder; videoBorder += "px"; document.querySelector(":root").style.setProperty("--video-border-color", "#000"); document.querySelector(":root").style.setProperty("--video-border", videoBorder); session.manual = session.manual === null ? false : session.manual; session.windowed = session.windowed === null ? false : session.windowed; } catch (e) { errorlog("variable css failed"); } } if (urlParams.has("bordercolor")) { try { session.borderColor = urlParams.get("bordercolor") || "#000"; document.querySelector(":root").style.setProperty("--video-border-color", session.borderColor); } catch (e) { errorlog("variable css failed"); } } if (urlParams.has("holdercolor")) { try { session.holderColor = urlParams.get("holdercolor") || session.borderColor || "#000"; if (parseInt(session.holderColor) == session.holderColor){ session.holderColor = "#"+session.holderColor; } document.querySelector(":root").style.setProperty("--video-holder-color", session.holderColor); } catch (e) { errorlog("variable css failed"); } } if (urlParams.has("color")) { session.colorVideosBackground = urlParams.get("color") || session.borderColor || "#000"; } if (urlParams.has("retry")) { session.forceRetry = parseInt(urlParams.get("retry")) || 30; } if (session.forceRetry) { clearInterval(session.forceRetryTimeout); session.forceRetryTimeout = setTimeout(function () { try { session.retryWatchInterval(); } catch (e) { log(e); clearTimeout(this); } }, session.forceRetry * 1000); } if (urlParams.get("dropbox")) { setupDropbox(urlParams.get("dropbox")).then(() => { log("Loaded dropbox SDK"); }).catch(e => errorlog(e)); } if (urlParams.has("gdrive")) { session.gdrive = {}; } try { if (urlParams.has("darkmode") || urlParams.has("nightmode") || urlParams.has("darktheme")) { session.darkmode = urlParams.get("darkmode") || urlParams.get("nightmode") || urlParams.get("darktheme") || null; if (session.darkmode === null || session.darkmode === "") { session.darkmode = true; } else if (darkmode == "false" || darkmode == "0" || darkmode == 0 || darkmode == "off") { session.darkmode = false; } } else if (urlParams.has("lightmode") || urlParams.has("lighttheme")) { session.darkmode = false; } else if (urlParams.has("whitemode") || urlParams.has("whitetheme")) { document.body.classList.remove('darktheme'); document.body.classList.add('whitetheme'); session.darkmode = false; } else if (session.studioSoftware) { session.darkmode = false; // prevent OBS from defaulting to dark mode, avoiding possible overlooked bugs. } else if (session.darkmode === null) { session.darkmode = getComputedStyle(document.querySelector(":root")).getPropertyValue("--color-mode").trim(); if (session.darkmode == "dark") { session.darkmode = true; } else { session.darkmode = false; } } if (session.darkmode) { document.body.classList.add("darktheme"); //document.querySelector(':root').style.setProperty('--background-color',"#02050c" ); } else { document.body.classList.remove("darktheme"); //document.querySelector(':root').style.setProperty('--background-color',"#141926" ); // already set as default. } } catch (e) { errorlog(e); console.warn("⚠️ If you are seeing this error, it's likely a third-party browser extension is breaking the site\n\nTry a different browser, incognito mode, or disable the problematic extension."); } if (urlParams.has("videodevice") || urlParams.has("vdevice") || urlParams.has("vd") || urlParams.has("device") || urlParams.has("d") || urlParams.has("vdo")) { session.videoDevice = urlParams.get("videodevice") || urlParams.get("vdevice") || urlParams.get("vd") || urlParams.get("device") || urlParams.get("d") || urlParams.get("vdo"); if (session.videoDevice === null) { session.videoDevice = "1"; } else if (session.videoDevice) { session.videoDevice = normalizeDeviceLabel(session.videoDevice); } if (session.videoDevice == "false") { session.videoDevice = 0; } else if (session.videoDevice == "0") { session.videoDevice = 0; } else if (session.videoDevice == "no") { session.videoDevice = 0; } else if (session.videoDevice == "off") { session.videoDevice = 0; } else if (session.videoDevice == "snapcam") { session.videoDevice = "snap_camera"; } else if (session.videoDevice == "canon") { session.videoDevice = "eos"; } else if (session.videoDevice == "camlink") { session.videoDevice = "cam_link"; } else if (session.videoDevice == "ndi") { session.videoDevice = "newtek_ndi_video"; } else if (session.videoDevice == "") { session.videoDevice = 1; } else if (session.videoDevice == "1") { session.videoDevice = 1; } else if (session.videoDevice == "default") { session.videoDevice = 1; } if (!urlParams.has("vdo")) { getById("videoMenu").style.display = "none"; getById("videoMenu").classList.add("hidden"); // getById("videoMenu2").style.display = "none"; // getById("videoMenu2").classList.add("hidden"); // getById("videoMenu3").style.display = "none"; // getById("videoMenu3").classList.add("hidden"); } log("session.videoDevice:" + session.videoDevice); } // audioDevice if (urlParams.has("audiodevice") || urlParams.has("adevice") || urlParams.has("ad") || urlParams.has("device") || urlParams.has("d") || urlParams.has("ado")) { session.audioDevice = urlParams.get("audiodevice") || urlParams.get("adevice") || urlParams.get("ad") || urlParams.get("device") || urlParams.get("d") || urlParams.get("ado"); if (session.audioDevice === null) { session.audioDevice = "1"; } else if (session.audioDevice) { session.audioDevice = normalizeDeviceLabel(session.audioDevice); } if (session.audioDevice == "false") { session.audioDevice = 0; } else if (session.audioDevice == "0") { session.audioDevice = 0; } else if (session.audioDevice == "no") { session.audioDevice = 0; } else if (session.audioDevice == "off") { session.audioDevice = 0; } else if (session.audioDevice == "") { session.audioDevice = 1; } else if (session.audioDevice == "1") { session.audioDevice = 1; } else if (session.audioDevice == "default") { session.audioDevice = 1; } else if (session.audioDevice == "ndi") { session.audioDevice = ["line_newtek_ndi_audio"]; } else { session.audioDevice = session.audioDevice.split(","); } getById("headphonesDiv").classList.add("hidden"); getById("headphonesDiv2").classList.add("hidden"); if (typeof session.audioDevice !== "object" && !urlParams.has("ado")) { getById("audioMenu").style.display = "none"; getById("audioScreenShare1").style.display = "none"; getById("audioMenu").classList.add("hidden"); getById("audioScreenShare1").classList.add("hidden"); } if (session.audioDevice) { // 0 or false, do not triger log("requestAudioStream..()"); try { await requestAudioStream(); } catch (e) { errorlog(e); } } } if (session.videoDevice === 0) { getById("previewWebcam").classList.add("miconly"); if (session.audioDevice === 0) { miniTranslate(getById("add_camera"), "click-start-to-join", "Click Start to Join"); getById("container-2").className = "column columnfade hidden"; getById("container-3").classList.add("skip-animation"); getById("container-3").classList.remove("pointer"); //delayedStartupFuncs.push([previewWebcam]); session.webcamonly = true; } else { miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); getById("container-3").classList.add("microphoneBackground"); } getById("container-3").title = getById("add_camera").innerText; } if (session.mobile) { getById("shareScreenGear").style.display = "none"; getById("dropButton").style.display = "none"; //getById("container-2").className = 'column columnfade hidden'; // Hide screen share on mobile session.screensharebutton = false; screensharesupport = false; if (session.audioDevice !== 0) { getById("flipcamerabutton").classList.remove("hidden"); } } if (urlParams.has("androidfix")) { session.AndroidFix = true; } if (urlParams.has("consent")) { session.consent = true; getById("consentWarning").classList.remove("hidden"); getById("consentWarning2").classList.remove("hidden"); } if (urlParams.has("autojoin") || urlParams.has("autostart") || urlParams.has("aj") || urlParams.has("as")) { session.autostart = true; } if (urlParams.has("blackout") || urlParams.has("blackoutmode") || urlParams.has("bo") || urlParams.has("bom")) { getById("blackoutmode").classList.remove("hidden"); if (urlParams.get("blackout") || urlParams.get("blackoutmode") || urlParams.get("bo") || urlParams.get("bom")) { blackoutMode(); } } if (session.dataMode) { delayedStartupFuncs.push([joinDataMode]); } else if (session.autostart) { if (session.screenshare !== false) { delayedStartupFuncs.push([publishScreen]); } if (session.consent) { setTimeout(function () { warnUser("⚠ Privacy warning: The director of this room can remotely switch your camera or microphone without permission.", 8000); }, 1500); } } if (urlParams.has("noiframe") || urlParams.has("noiframes") || urlParams.has("nif") || urlParams.has("nowebsite")) { session.noiframe = urlParams.get("noiframe") || urlParams.get("noiframes") || urlParams.get("nif") || urlParams.get("nowebsite"); if (!session.noiframe) { session.noiframe = []; } else { session.noiframe = session.noiframe.split(","); } log("disable iframe playback"); log(session.noiframe); } if (urlParams.has("exclude") || urlParams.has("ex")) { session.exclude = urlParams.get("exclude") || urlParams.get("ex"); if (!session.exclude) { session.exclude = false; } else { session.exclude = session.exclude.split(","); } log("exclude audio/video playback"); log(session.exclude); } if (urlParams.has("noscreenshare") || urlParams.has("noscreenshares") || urlParams.has("noscreen") || urlParams.has("noscreens")) { session.noScreenShare = true; log("disable screen share playback"); } if (urlParams.has("screenp2p") || urlParams.has("noscreenwhep")) { session.screenWhepPreference = "p2p"; } if (urlParams.has("screenwheponly")) { session.screenWhepPreference = "whep"; } if (urlParams.has("screenwhep")) { const pref = parseScreenToggleParam(urlParams.get("screenwhep")); if (pref === false) { session.screenWhepPreference = "p2p"; } else if (pref === true) { session.screenWhepPreference = "whep"; } } function parseScreenToggleParam(value) { if (value === null || value === undefined) { return true; } const normalized = ("" + value).trim().toLowerCase(); if (!normalized.length) { return true; } if (["0", "false", "no", "off", "disable", "disabled", "none"].includes(normalized)) { return false; } if (["1", "true", "yes", "on", "enable", "enabled"].includes(normalized)) { return true; } return true; } if (urlParams.has("allowscreenvideo")) { session.screenVideoOverride = parseScreenToggleParam(urlParams.get("allowscreenvideo")); log("screen share video override: " + session.screenVideoOverride); } if (urlParams.has("allowscreenaudio")) { session.screenAudioOverride = parseScreenToggleParam(urlParams.get("allowscreenaudio")); log("screen share audio override: " + session.screenAudioOverride); } if (urlParams.has("excludeaudio") || urlParams.has("exaudio") || urlParams.has("silence")) { session.excludeaudio = urlParams.get("excludeaudio") || urlParams.get("exaudio") || urlParams.get("silence"); if (!session.excludeaudio) { session.excludeaudio = false; } else { session.excludeaudio = session.excludeaudio.split(","); } log("exclude audio playback"); log(session.excludeaudio); } 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 = []; } else { session.novideo = session.novideo.split(","); } log("disable video playback"); 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 = []; } else { session.noaudio = session.noaudio.split(","); } log("disable audio playback"); } if (urlParams.has("nodirectoraudio")) { session.nodirectoraudio = true; log("disable audio playback from Directors"); } if (urlParams.has("nodirectorvideo")) { session.nodirectoraudio = true; log("disable audio playback from Directors"); } if (urlParams.has("forceios")) { log("allow iOS to work in video group chat; for this user at least"); session.forceios = true; } if (urlParams.has("nocursor") || urlParams.has("hidecursor") || urlParams.has("nomouse") || urlParams.has("hidemouse")) { // on the screen, not in screen share session.nocursor = true; log("DISABLE CURSOR"); var styletmp = document.createElement("style"); styletmp.innerHTML = ` video { margin: 0; padding: 0; overflow: hidden; cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=), none; user-select: none; } `; document.head.appendChild(styletmp); } if (urlParams.has("cursor") || urlParams.has("screensharecursor")) { session.screensharecursor = true; } if (urlParams.has("distort")) { session.voicechanger = 1; } if (urlParams.has("dtx") || urlParams.has("usedtx")) { session.dtx = true; session.cbr = 0; // no point dtx on if cbr is on, right? } if (urlParams.has("youtube")) { // Set with a Youtube v3 clientID + "," + API key, then run YoutubeChatInterface(); session.youtubeKey = urlParams.get("youtube") || ""; //YoutubeChatInterface(); // queries Youtube for chat messages. Forwards them to the parent IFRAME only at the moment. } if (urlParams.has("vbr")) { session.cbr = 0; getById("whipoutvbrcbr").classList.add("hidden"); } else if (urlParams.has("cbr")) { session.cbr = 1; getById("whipoutvbrcbr").classList.add("hidden"); } if (urlParams.has("order")) { session.order = parseInt(urlParams.get("order")) || 1; } if (urlParams.has("orderby")) { session.orderby = urlParams.get("orderby") || "id"; // "label" also an option; the default is stream ID tho. } if (urlParams.has("slotmode") || urlParams.has("slotsmode")) { session.slotmode = parseInt(urlParams.get("slotmode")) || parseInt(urlParams.get("slotsmode")) || 1; } if (urlParams.has("slot")) { var slotValue = parseInt(urlParams.get("slot")); session.slot = isNaN(slotValue) ? false : slotValue; // 0 = exclude from slots, N = prefer slot N, false = auto-assign } if (urlParams.has("slots")) { session.slots = parseInt(urlParams.get("slots")) || 4; // first N slots can be filled } else if (urlParams.has('slotslist')) { // select which slots you want to be processed session.slotsList = urlParams.get('slotslist').split(',').map(slot => parseInt(slot.trim())).filter(slot => !isNaN(slot)); if (!session.slotsList.length){ session.slotsList = false; } } if (urlParams.has("maxslots")) { // hard coded default is 12; if &maxslots used, it changes to 20 unless value passed. session.maxAvailableSlots = parseInt(urlParams.get("maxslots")) || session.maxAvailableSlots; } if (session.slotmode){ populateSlotPicker(); } if (urlParams.has("alpha")) { session.alpha = true; } if (urlParams.has("chunked") || urlParams.has("chunk")) { session.chunked = parseInt(urlParams.get("chunked")) || parseInt(urlParams.get("chunk")) || 2500; // sender side; enables to allows. // session.alpha = true; if (Firefox || SafariVersion) { if (!session.cleanOutput) { warnUser("Only Chromium-based browsers support chunked mode.\n\nPlease switch to Chrome or another compatible browser to use &chunked mode."); } session.chunked = false; console.warn("Disabling chunked mode since not using a compatible browser."); } } if (urlParams.has("chunkedbuffer") || urlParams.has("sendingbuffer")) { session.sendingBuffer = parseInt(urlParams.get("chunkedbuffer")) || parseInt(urlParams.get("sendingbuffer")) || 5000; // sender side; enables to allows. } const chunkProfiles = { mobile: { chunkfec: 3, chunknack: true, chunkbuffer: 900, chunkbufferfloor: 600, chunkbufferceil: 1600, chunkjitterslack: 250, chunkadapt: "framerate", chunkadaptfloor: 320, chunkadaptceil: 1400, chunkadaptthreshold: 260, chunkadaptmaxdrop: 10, chunkadaptinterval: 1200 }, balanced: { chunkfec: 4, chunknack: true, chunkbuffer: 750, chunkbufferfloor: 450, chunkbufferceil: 1400, chunkjitterslack: 220, chunkadapt: "hybrid", chunkadaptfloor: 420, chunkadaptceil: 2600, chunkadaptthreshold: 340, chunkadaptmaxdrop: 6, chunkadaptinterval: 900 }, desktop: { chunkfec: 5, chunknack: true, chunkbuffer: 620, chunkbufferfloor: 400, chunkbufferceil: 1100, chunkjitterslack: 180, chunkadapt: "bitrate", chunkadaptfloor: 580, chunkadaptceil: 4200, chunkadaptthreshold: 360, chunkadaptmaxdrop: 4, chunkadaptinterval: 700 } }; function applyChunkPreset(preset) { if (!preset) { return; } Object.keys(preset).forEach(key => { const value = preset[key]; const sessionKey = key; session[sessionKey] = value; }); } if (urlParams.has("chunkprofile")) { const profileName = (urlParams.get("chunkprofile") || "").toLowerCase(); if (chunkProfiles[profileName]) { session.chunkprofile = profileName; applyChunkPreset(chunkProfiles[profileName]); } } function parseIntegerParam(name, target, clamp = null) { if (!urlParams.has(name)) { return; } let value = parseInt(urlParams.get(name)); if (!Number.isFinite(value)) { return; } if (clamp && Array.isArray(clamp)) { const [min, max] = clamp; if (typeof min === "number") { value = Math.max(min, value); } if (typeof max === "number") { value = Math.min(max, value); } } session[target] = value; } function parseBooleanParam(name, target) { if (!urlParams.has(name)) { return; } const raw = urlParams.get(name); if (raw === null || raw === "" || raw === "1" || raw.toLowerCase() === "true") { session[target] = true; } else if (raw.toLowerCase() === "0" || raw.toLowerCase() === "false") { session[target] = false; } else { session[target] = true; } } parseIntegerParam("chunkfec", "chunkfec", [0, 12]); parseBooleanParam("chunknack", "chunknack"); parseIntegerParam("chunkbuffer", "chunkbuffer", [0, 30000]); parseIntegerParam("chunkbufferfloor", "chunkbufferfloor", [0, 30000]); parseIntegerParam("chunkbufferceil", "chunkbufferceil", [0, 60000]); parseIntegerParam("chunkjitterslack", "chunkjitterslack", [0, 10000]); if (urlParams.has("chunkadapt")) { const adaptMode = (urlParams.get("chunkadapt") || "").toLowerCase(); if (["bitrate", "framerate", "hybrid"].includes(adaptMode)) { session.chunkadapt = adaptMode; } } parseIntegerParam("chunkadaptfloor", "chunkadaptfloor", [0, 10000]); parseIntegerParam("chunkadaptceil", "chunkadaptceil", [0, 100000]); parseIntegerParam("chunkadaptthreshold", "chunkadaptthreshold", [0, 10000]); parseIntegerParam("chunkadaptmaxdrop", "chunkadaptmaxdrop", [0, 120]); parseIntegerParam("chunkadaptinterval", "chunkadaptinterval", [100, 60000]); parseIntegerParam("chunkretry", "chunkretry", [0, 60000]); parseIntegerParam("chunkcache", "chunkcache", [0, 60000]); parseIntegerParam("chunkchunksize", "chunkchunksize", [2048, 65536]); if (urlParams.has("nochunk") || urlParams.has("nochunked")) { // viewer side session.nochunk = true; } if (urlParams.has("nochunkaudio") || urlParams.has("nochunkedaudio")) { // viewer side session.nochunkaudio = true; } if (urlParams.has("audiobuffer") || urlParams.has("bufferaudio")) { // viewer side session.audioBuffer = urlParams.get("audiobuffer") || urlParams.get("bufferaudio") || 0; session.audioBuffer = parseInt(session.audioBuffer); } //if (urlParams.has('viewchunked') || urlParams.has('viewchunk') || urlParams.has('allowchunked') || urlParams.has('allowchunk')) { // viewer side // session.forceChunked = true; //} if (urlParams.has("token")) { session.token = urlParams.get("token") || false; // checkToken(); // this is sycnhonous } if (urlParams.has("maindirectorpassword") || urlParams.has("maindirpass")) { session.mainDirectorPassword = urlParams.get("maindirectorpassword") || urlParams.get("maindirpass") || false; if (!session.mainDirectorPassword) { window.focus(); session.mainDirectorPassword = await promptAlt(getTranslation("director-password"), true, true); if (session.mainDirectorPassword) { session.mainDirectorPassword = session.mainDirectorPassword.trim(); try { session.mainDirectorPassword = decodeURIComponent(session.mainDirectorPassword); } catch (e) { errorlog(e); } } } // registerToken(); } if (urlParams.has("debug")) { DebugLog = true; if (!errorReport) { errorReport = []; } if (urlParams.get("debug") == "1") { debugStart(); } else if (urlParams.get("debug")) { debugStart(urlParams.get("debug")); } } if (urlParams.has("group") || urlParams.has("groups")) { session.group = urlParams.get("group") || urlParams.get("groups") || ""; session.group = session.group.split(","); } if (urlParams.has("groupview") || urlParams.has("viewgroup") || urlParams.has("gv")) { session.groupView = urlParams.get("groupview") || urlParams.get("viewgroup") || urlParams.get("gv") || ""; session.groupView = session.groupView.split(","); } if (urlParams.has("groupaudio") || urlParams.has("ga")) { session.groupAudio = true; } if (urlParams.has("groupmode") || urlParams.has("gm")) { session.allowNoGroup = true; } if (urlParams.has("host")) { session.roomhost = true; } 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("sensorfilter") || urlParams.has("sensorsfilter") || urlParams.has("filtersensor") || urlParams.has("filtersensors")) { session.sensorDataFilter = urlParams.get("sensorfilter") || urlParams.get("sensorsfilter") || urlParams.get("filtersensor") || urlParams.get("filtersensors") || ""; session.sensorDataFilter = session.sensorDataFilter.split(","); // ["pos","lin","ori","mag","gyro","acc"]; } if (urlParams.has("webxrbridge") || urlParams.has("externalsensors") || urlParams.has("sensorsbridge")) { session.externalSensorBridge = true; session.externalSensorOrigin = urlParams.get("sensorsorigin") || ""; } if (urlParams.has("ptime")) { session.ptime = parseInt(urlParams.get("ptime")) || 20; if (session.ptime < 10) { session.ptime = 10; } } 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")) { session.maxptime = parseInt(urlParams.get("maxptime")) || 60; if (session.maxptime < 10) { session.maxptime = 10; } if (session.maxptime > 300) { session.maxptime = 300; } } if (urlParams.has("contenthint") || urlParams.has("contenttype") || urlParams.has("content") || urlParams.has("hint")) { session.contentHint = urlParams.get("contenthint") || urlParams.get("contenttype") || urlParams.get("content") || urlParams.get("hint") || "detail"; } if (urlParams.has("audiocontenthint") || urlParams.has("audiocontenttype") || urlParams.has("audiocontent") || urlParams.has("audiohint")) { session.audioContentHint = urlParams.get("audiocontenthint") || urlParams.get("audiocontenttype") || urlParams.get("audiocontent") || urlParams.get("audiohint") || "music"; } if (urlParams.has("screensharecontenthint") || urlParams.has("sscontenthint") || urlParams.has("screensharecontenttype") || urlParams.has("sscontent") || urlParams.has("sshint")) { session.screenshareContentHint = urlParams.get("screensharecontenthint") || urlParams.get("sscontenthint") || urlParams.get("screensharecontenttype") || urlParams.get("sscontent") || urlParams.get("sshint") || "detail"; } if (urlParams.has("vred")) { session.videoErrorCorrection = true; } if (urlParams.has("pvred")) { session.preferredVideoErrorCorrection = true; } if (urlParams.has("codec") || urlParams.has("codecs") || urlParams.has("videocodec")) { log("codecs CHANGED"); session.codecs = urlParams.get("codec") || urlParams.get("codecs") || urlParams.get("videocodec") || false; if (session.codecs) { session.codecs = session.codecs.toLowerCase(); session.codecs = session.codecs.split(","); if (session.codecs.length) { session.codec = session.codecs.shift(); if (!session.codec) { session.codec = false; session.codecs = false; } if (!session.codecs.length) { session.codecs = false; } } else { session.codecs = false; } } } else if (OperaGx) { session.codec = "vp8"; warnlog("Defaulting to VP8 manually, as H264 with remote iOS devices is not supported"); } if (urlParams.has("redaudio")) { // just for experimenting session.redAudio = true; } if (urlParams.has("fecaudio")) { // session.fecAudio = true; } if (urlParams.has("predaudio")) { // session.predAudio = true; } if (urlParams.has("pfecaudio")) { // session.pfecAudio = true; } if (urlParams.has("audiocodec")) { log("CODEC CHANGED"); session.audioCodec = urlParams.get("audiocodec") || false; if (session.audioCodec) { session.audioCodec = session.audioCodec.toLowerCase(); } } if (session.audioCodec && session.audioCodec == "red") { session.audiobitratePRO = 216; // higher than this seems to break the RED mode. default 256. } if (urlParams.has("preferaudiocodec")) { log("PREFER CODEC CHANGED"); session.preferAudioCodec = urlParams.get("preferaudiocodec") || false; if (session.preferAudioCodec) { session.preferAudioCodec = session.preferAudioCodec.toLowerCase(); } } if (urlParams.has("prefervideocodec")) { log("PREFER VIDEO CODEC CHANGED"); session.preferVideoCodec = urlParams.get("prefervideocodec") || false; if (session.preferVideoCodec) { session.preferVideoCodec = session.preferVideoCodec.toLowerCase(); } } if (urlParams.has("scenelinkcodec")) { // this is mainly for a niche iframe API use log("codecGroupFlag CHANGED"); session.codecGroupFlag = urlParams.get("scenelinkcodec") || false; if (session.codecGroupFlag) { session.codecGroupFlag = "&codec=" + session.codecGroupFlag.toLowerCase(); } } if (session.codecGroupFlag) { getById("codecGroupFlag").disabled = true; } if (urlParams.has("scenelinkbitrate")) { // this is mainly for a niche iframe API use log("bitrateGroupFlag CHANGED"); session.bitrateGroupFlag = urlParams.get("scenelinkbitrate") || false; if (session.bitrateGroupFlag) { session.bitrateGroupFlag = "&totalbitrate=" + parseInt(session.bitrateGroupFlag); } } if (urlParams.has("h264profile")) { session.h264profile = urlParams.get("h264profile") || "42e01f"; // 42001f session.h264profile = session.h264profile.substring(0, 6); session.h264profile = session.h264profile.toLowerCase(); if (session.h264profile == "0") { session.h264profile = false; } else if (session.h264profile == "off") { session.h264profile = false; } else if (session.h264profile == "disabl") { session.h264profile = false; } else if (session.h264profile == "defaul") { session.h264profile = false; } else if (session.h264profile == "false") { session.h264profile = false; } } else if (session.codec === "hardware" && Android) { // same as &h264profile, but easier for me to remember. I'll try to automate this in the future. session.codec = "h264"; session.h264profile = "42e01f"; } if (urlParams.has("nofec")) { // disables error control / throttling -- currently on audio session.noFEC = true; } if (urlParams.has("nonacks") || urlParams.has("nonack")) { // disables error control / throttling. session.noNacks = true; } if (urlParams.has("nopli")) { // disables error control / throttling. session.noPLIs = true; } if (urlParams.has("noremb")) { // disables Receiver Estimated Maximum Bitrate (throttling) session.noREMB = true; } 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 = parseFloat(urlParams.get("scale")) || 100; } session.dynamicScale = false; // default true } else { if (urlParams.has("viewwidth") || urlParams.has("vw")) { session.viewwidth = urlParams.get("viewwidth") || urlParams.get("vw") || false; if (session.viewwidth) { session.viewwidth = parseInt(session.viewwidth); } session.dynamicScale = false; // default true } if (urlParams.has("viewheight") || urlParams.has("vh")) { session.viewheight = urlParams.get("viewheight") || urlParams.get("vh") || false; session.dynamicScale = false; // default true if (session.viewheight) { session.viewheight = parseInt(session.viewheight); } } } if (urlParams.has("sharperscreen")) { // sets scale to 100 for inbound screenshares only session.sharperScreen = true; } if (urlParams.has("mcscale") || urlParams.has("meshcastscale") || urlParams.has("woscale") || urlParams.has("whipoutscale")) { session.whipOutScale = parseFloat(urlParams.get("mcscale")) || parseFloat(urlParams.get("meshcastscale")) || parseFloat(urlParams.get("woscale")) || parseFloat(urlParams.get("whipoutscale")) || 100; } if (isIFrame) { getById("helpbutton").style.display = "none"; getById("helpbutton").style.opacity = 0; getById("reportbutton").style.display = "none"; getById("reportbutton").style.opacity = 0; getById("calendarButton").style.display = "none"; getById("calendarButton").style.opacity = 0; getById("chatBody").innerHTML = ""; } if (urlParams.has("poke")){ session.poke = urlParams.get("poke").replace(/[\W]+/g, "_").replace(/_+/g, "_") || true; } if (urlParams.has("beep") || urlParams.has("notify") || urlParams.has("tone")) { let beepValue = urlParams.get("beep") || urlParams.get("notify") || urlParams.get("tone") || ""; let beepTypes = []; if (beepValue) { beepTypes = beepValue .split(",") .map(type => type.trim().toLowerCase()) .filter(type => type !== ""); session.beepToNotify = beepTypes.length ? beepTypes : true; } else { beepTypes = []; session.beepToNotify = true; // enable all, since nothing was specified } if (beepTypes.length === 0 || beepTypes.includes("knock")) { // Allow callers to request the louder knock tone without extra flags session.knockToneEnabled = true; } if (beepTypes.length === 0 || beepTypes.includes("join")) { const addtone = createAudioElement(); addtone.id = "jointone"; addtone.src = "./media/join.mp3"; getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling); } if (beepTypes.length === 0 || beepTypes.includes("leave")) { const addtone = createAudioElement(); addtone.id = "leavetone"; addtone.src = "./media/leave.mp3"; getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling); } if (!Notification) { warnlog("Desktop notifications are not available in your browser."); } else if (Notification.permission !== "granted") { Notification.requestPermission(); } } if (urlParams.has("r2d2")) { /* var addtone = createAudioElement(); addtone.id = "jointone"; addtone.src = "./media/join.mp3"; getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling) var addtone = createAudioElement(); addtone.id = "leavetone"; addtone.src = "./media/leave.mp3"; getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling) */ getById("testtone").innerHTML = ""; getById("testtone").src = "./media/robot.mp3"; session.beepToNotify = true; } if (urlParams.get("custombeep")) { updateAudioSource(urlParams.get("custombeep"), "testtone"); } if (urlParams.get("customleave")) { updateAudioSource(urlParams.get("customleave"), "leavetone"); } if (urlParams.get("customjoin")) { updateAudioSource(urlParams.get("customjoin"), "jointone"); } if (urlParams.has("beepvolume")) { const volume = parseInt(urlParams.get("beepvolume")) / 100 || 0; ["testtone", "jointone", "leavetone"].forEach(id => { const audio = document.getElementById(id); if (!audio) return; try { if (volume > 1 && session.audioCtx) { audio.volume = 1; // Set base volume to 100% audio.crossOrigin = "anonymous"; // Try to enable CORS const source = session.audioCtx.createMediaElementSource(audio); const gainNode = session.audioCtx.createGain(); gainNode.gain.value = volume; source.connect(gainNode); gainNode.connect(session.audioCtx.destination); audio.gainNode = gainNode; console.warn("note: If the audio file is protected by CORS, increasing the volume will cause it to fail"); } else { audio.volume = volume; } } catch (e) { console.warn("Volume boost failed, falling back to normal volume", e); audio.volume = Math.min(1, volume); // Fallback to normal volume, capped at 100% } }); } if (urlParams.has("easyexit") || urlParams.has("ee")) { session.noExitPrompt = true; } if (urlParams.has("entrymsg") || urlParams.has("welcome") || urlParams.has("welcomeb64")) { session.welcomeMessage = urlParams.get("entrymsg") || urlParams.get("welcome") || urlParams.get("welcomeb64"); if (urlParams.get("welcomeb64")) { try { session.welcomeMessage = atob(session.welcomeMessage); } catch (e) {} } try { session.welcomeMessage = session.welcomeMessage.replace(/(\r\n|\n|\r)/gm, " "); session.welcomeMessage = decodeURIComponent(session.welcomeMessage); } catch (e) {} } if (urlParams.has("welcomehtml")) { session.welcomeHTML = urlParams.get("welcomehtml"); try { session.welcomeHTML = atob(session.welcomeHTML); } catch (e) {} try { session.welcomeHTML = session.welcomeHTML.replace(/(\r\n|\n|\r)/gm, " "); session.welcomeHTML = decodeURIComponent(session.welcomeHTML); } catch (e) {} } if (urlParams.has("welcomeimage") || urlParams.has("welcomeimg")) { session.welcomeImage = urlParams.get("welcomeimage") || urlParams.get("welcomeimg"); try { session.welcomeImage = decodeURIComponent(session.welcomeImage); } catch (e) {} } if (urlParams.has("mixminus") || urlParams.has("mm")) { session.mixMinus = true; // Director/co-director mix-minus: director mixes audio and sends custom mix to each guest if (session.director || session.codirector) { session.directorMixMinus = true; session.mixMinusState = {}; // Per-guest mix-minus state session.mixMinusDefaults = { allGuestsEnabled: true, // Default state for new guests includeDirectorAudio: true, // Include director's audio by default includeAllGuests: true // Include all other guests by default }; } } if (urlParams.has("clearstorage") || urlParams.has("clear")) { clearStorage(); } if (urlParams.has("videobitrate") || urlParams.has("bitrate") || urlParams.has("vb")) { session.bitrate = urlParams.get("videobitrate") || urlParams.get("bitrate") || urlParams.get("vb") || 8000; 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; } log("BITRATE ENABLED"); log(session.bitrate); } } if (urlParams.has("maxvideobitrate") || urlParams.has("maxbitrate") || urlParams.has("maxvb") || urlParams.has("mvb")) { session.maxvideobitrate = urlParams.get("maxvideobitrate") || urlParams.get("maxbitrate") || urlParams.get("maxvb") || urlParams.get("mvb"); 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") || urlParams.has("totalbitrate") || urlParams.has("tb")) { session.totalRoomBitrate = urlParams.get("totalroombitrate") || urlParams.get("totalroomvideobitrate") || urlParams.get("trb") || urlParams.get("totalbitrate") || urlParams.get("tb") || ""; if (session.totalRoomBitrate.split(",").length > 1) { if (session.mobile) { session.totalRoomBitrate = session.totalRoomBitrate.split(",")[1]; } else { session.totalRoomBitrate = session.totalRoomBitrate.split(",")[0]; } } if ((session.totalRoomBitrate == "false") || (session.totalRoomBitrate == "off")){ session.totalRoomBitrate = 0; } session.totalRoomBitrate = parseInt(session.totalRoomBitrate) || 0; if (session.totalRoomBitrate < 1) { session.totalRoomBitrate = 0; } log("totalRoomBitrate ENABLED"); log(session.totalRoomBitrate); } if (session.totalRoomBitrate === false) { session.totalRoomBitrate = session.bitrate || session.totalRoomBitrate_default; // sneaky sneaky } else { session.totalRoomBitrate_default = session.totalRoomBitrate; // trb_default doesn't change dynamically, but trb can (per director I guess) } if (session.totalRoomBitrate_default > 4000) { getById("trbSettingInput").max = Math.ceil(session.totalRoomBitrate_default); } if (urlParams.has("maxtotalscenebitrate") || urlParams.has("totalscenebitrate") || urlParams.has("mtsb") || urlParams.has("tsb") || urlParams.has("totalbitrate") || urlParams.has("tb")) { session.totalSceneBitrate = urlParams.get("maxtotalscenebitrate") || urlParams.get("totalscenebitrate") || urlParams.get("mtsb") || urlParams.get("tsb") || urlParams.get("totalbitrate") || urlParams.get("tb") || false; if (session.totalSceneBitrate) { session.totalSceneBitrate = parseInt(session.totalSceneBitrate); } } if (urlParams.has("blur")) { session.blurBackground = urlParams.get("blur") || 10; session.blurBackground = parseInt(session.blurBackground) || 10; if (session.blurBackground < 0) { session.blurBackground = false; } session.structure = true; } if (urlParams.has("limittotalbitrate") || urlParams.has("ltb")) { session.limitTotalBitrate = urlParams.get("limittotalbitrate") || urlParams.get("ltb") || "2500"; if (session.limitTotalBitrate.split(",").length > 1) { if (session.mobile) { session.limitTotalBitrate = session.limitTotalBitrate.split(",")[1]; } else { session.limitTotalBitrate = session.limitTotalBitrate.split(",")[0]; } } session.limitTotalBitrate = parseInt(session.limitTotalBitrate); getById("limittotalbitrate_director").classList.remove("hidden"); } if (session.limitTotalBitrate) { if (session.limitTotalBitrate > session.limitTotalBitrate_defaultMax) { getById("ltbSettingInputManual").max = Math.ceil(session.limitTotalBitrate); } getById("ltbSettingInputManual").value = session.limitTotalBitrate; getById("ltbSettingInput").value = session.limitTotalBitrate; getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled"; } if (urlParams.has("mcscreensharebitrate") || urlParams.has("mcssbitrate") || urlParams.has("whipoutscreensharebitrate") || urlParams.has("wossbitrate")) { session.whipOutScreenShareBitrate = urlParams.get("mcscreensharebitrate") || urlParams.get("mcssbitrate") || urlParams.get("whipoutscreensharebitrate") || urlParams.get("wossbitrate") || 2500; session.whipOutScreenShareBitrate = parseInt(session.whipOutScreenShareBitrate); } if (urlParams.has("mcscreensharecodec") || urlParams.has("mcsscodec") || urlParams.has("whipoutscreensharecodec") || urlParams.has("wosscodec")) { session.whipOutScreenShareCodec = urlParams.get("mcscreensharecodec") || urlParams.get("mcsscodec") || urlParams.get("whipoutscreensharecodec") || urlParams.get("wosscodec") || false; } if (session.whipOutScreenShareCodec) { session.whipOutScreenShareCodec = session.whipOutScreenShareCodec.toLowerCase(); } if (urlParams.has("mccodec") || urlParams.has("meshcastcodec") || urlParams.has("whipoutcodec") || urlParams.has("whipoutvideocodec") || urlParams.has("woc") || urlParams.has("wovc")) { session.whipOutCodec = urlParams.get("mccodec") || urlParams.get("meshcastcodec") || urlParams.get("whipoutcodec") || urlParams.get("whipoutvideocodec") || urlParams.get("woc") || urlParams.get("wovc") || false; getById("whipoutcodecGroupFlag").classList.add("hidden"); } if (session.whipOutCodec) { session.whipOutCodec = session.whipOutCodec.toLowerCase(); if (session.whipOutCodec == "h264") { if (Firefox) { session.whipOutCodec = false; } } if (session.whipOutCodec) { session.whipOutCodec = session.whipOutCodec.split(","); } getById("whipoutcodecGroupFlag").classList.add("hidden"); } if (urlParams.has("whipoutaudiocodec") || urlParams.has("woac")) { session.whipOutAudioCodec = urlParams.get("whipoutaudiocodec") || urlParams.get("woac") || false; // getById("whipoutaudiocodecGroupFlag").classList.add("hidden"); // havne't added this in yet as an actual html element } if (urlParams.has("mcab") || urlParams.has("mcaudiobitrate") || urlParams.has("meshcastab") || urlParams.has("meshcastaudiobitrate ") || urlParams.has("whipoutaudiobitrate") || urlParams.has("woab")) { session.whipOutAudioBitrate = urlParams.get("mcab") || urlParams.get("mcaudiobitrate") || urlParams.get("meshcastab") || urlParams.get("meshcastaudiobitrate ") || urlParams.get("whipoutaudiobitrate") || urlParams.get("woab") || false; if (session.whipOutAudioBitrate) { session.whipOutAudioBitrate = parseInt(session.whipOutAudioBitrate); } getById("whipoutaudiobitrate").classList.add("hidden"); } if (urlParams.has("mcb") || urlParams.has("mcbitrate") || urlParams.has("meshcastbitrate") || urlParams.has("whipoutvideobitrate") || urlParams.has("wovb")) { session.whipOutVideoBitrate = urlParams.get("mcb") || urlParams.get("mcbitrate") || urlParams.get("meshcastbitrate") || urlParams.get("whipoutvideobitrate") || urlParams.get("wovb") || false; if (session.whipOutVideoBitrate) { session.whipOutVideoBitrate = parseInt(session.whipOutVideoBitrate); } getById("whipoutbitrateGroupFlag").classList.add("hidden"); getById("whipoutvbrcbr").classList.add("hidden"); } 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")) { session.width = urlParams.get("width") || urlParams.get("w"); session.width = parseInt(session.width); } if (urlParams.has("quality") || urlParams.has("q")) { try { session.quality = urlParams.get("quality") || urlParams.get("q") || "0"; if (session.quality.toLowerCase() == "4k") { session.quality = -2; } else if (session.quality.toLowerCase() == "2160p") { session.quality = -2; } else if (session.quality.toLowerCase() == "2160") { session.quality = -2; } else if (session.quality.toLowerCase() == "2k") { session.quality = -3; } else if (session.quality.toLowerCase() == "1440p") { session.quality = -3; } else if (session.quality.toLowerCase() == "1440") { session.quality = -3; } else if (session.quality.toLowerCase() == "hd") { // session.quality = 1; } else if (session.quality.toLowerCase() == "720p") { // session.quality = 1; } else if (session.quality.toLowerCase() == "720") { // session.quality = 1; } else if (session.quality.toLowerCase() == "fullhd") { session.quality = 0; } else if (session.quality.toLowerCase() == "1080p") { session.quality = 0; } else if (session.quality.toLowerCase() == "1080") { session.quality = 0; } else if (session.quality.toLowerCase() == "high") { session.quality = 0; } else if (session.quality.toLowerCase() == "360p") { session.quality = 2; } else if (session.quality.toLowerCase() == "360") { session.quality = 2; } else if (session.quality.toLowerCase() == "low") { session.quality = 2; } session.quality = parseInt(session.quality); getById("gear_screen").parentNode.removeChild(getById("gear_screen")); getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); } catch (e) { errorlog(e); } } else if (urlParams.has("fullhd") || urlParams.has("1080p")) { session.quality = 0; getById("gear_screen").parentNode.removeChild(getById("gear_screen")); getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); } else if (urlParams.has("4k")) { session.quality = -2; getById("gear_screen").parentNode.removeChild(getById("gear_screen")); getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); } if (urlParams.has("sink")) { session.sink = urlParams.get("sink"); } else if (urlParams.has("outputdevice") || urlParams.has("od") || urlParams.has("audiooutput")) { session.outputDevice = urlParams.get("outputdevice") || urlParams.get("od") || urlParams.get("audiooutput") || null; if (session.outputDevice) { session.outputDevice = normalizeDeviceLabel(session.outputDevice); } else { session.outputDevice = null; getById("headphonesDiv3").style.display = "none"; // } if (session.outputDevice) { try { enumerateDevices().then(function (deviceInfos) { for (let i = 0; i !== deviceInfos.length; ++i) { if (deviceInfos[i].kind === "audiooutput") { if (normalizeDeviceLabel(deviceInfos[i].label).includes(session.outputDevice)) { session.sink = deviceInfos[i].deviceId; log("AUDIO OUT DEVICE: " + deviceInfos[i].deviceId); break; } } } }); } catch (e) {} } getById("headphonesDiv").classList.add("hidden"); getById("headphonesDiv2").classList.add("hidden"); } else if (session.sink) { if (session.sink == "default") { session.sink = false; } else { enumerateDevices().then(function (deviceInfos) { var matched = false; for (let i = 0; i !== deviceInfos.length; ++i) { if (deviceInfos[i].kind === "audiooutput") { if (deviceInfos[i].deviceId == session.sink) { matched = true; break; } } } if (!matched) { session.sink = false; // make sure any saved output device exists. } }); } } if (session.studioSoftware || navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { session.fullscreen = true; } else if (urlParams.has("fullscreen")) { session.fullscreen = true; } if (urlParams.has("stats")) { if (urlParams.get("stats") == "0") { session.statsMenu = false; } else if (urlParams.get("stats") == "false") { session.statsMenu = false; } else if (urlParams.get("stats") == "off") { session.statsMenu = false; } else { session.statsMenu = true; } } else if (urlParams.has("nostats")) { session.statsMenu = false; } if (session.statsMenu === false) { // hide menu option try { document.queryselector('[data-action="ShowStats"]').parentNode.classList.add("hidden"); } catch (e) {} } if (urlParams.has("statsinterval")) { session.statsInterval = parseInt(urlParams.get("statsinterval")) || 3000; // milliseconds. interval of requesting stats of remote guests } if (urlParams.has("cleandirector") || urlParams.has("cdv")) { session.cleanDirector = true; } if (urlParams.has("hidetranslate")) { getById("translateButton").style.display = "none"; } if (session.cleanOutput) { session.screensharebutton = false; getById("translateButton").style.display = "none"; getById("credits").style.display = "none"; // getById("legal").style.display = "none"; getById("header").style.display = "none"; getById("controlButtons").classList.add("hidden"); getById("helpbutton").style.display = "none"; getById("helpbutton").style.opacity = 0; getById("reportbutton").style.display = "none"; getById("reportbutton").style.opacity = 0; getById("calendarButton").style.display = "none"; getById("calendarButton").style.opacity = 0; document.documentElement.style.setProperty("--myvideo-background", "#0000"); var styleTmp = document.createElement("style"); styleTmp.innerHTML = ` video { background-image: none; } `; document.head.appendChild(styleTmp); } if (urlParams.has("ssb") || urlParams.has("screensharebutton")) { session.screensharebutton = true; } 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; getById("obsState").classList.add("noheader"); } else if (urlParams.has("showheader")) { // needs to happen the room and permaid applications getById("header").style.display = "inherit"; getById("header").style.opacity = 1; setTimeout(function(){ getById("header").classList.remove("hidden"); getById("head2").classList.remove("hidden"); },100); } else if (session.studioSoftware) { getById("header").style.display = "none"; getById("header").style.opacity = 0; } if (urlParams.has("minidirector")) { try { var cssStylesheet = document.createElement("link"); cssStylesheet.rel = "stylesheet"; cssStylesheet.type = "text/css"; cssStylesheet.media = "screen"; cssStylesheet.href = "minidirector.css"; document.getElementsByTagName("head")[0].appendChild(cssStylesheet); } catch (e) { errorlog(e); } } if (urlParams.has("postinterval")) { // interval to post snapimage images session.postInterval = urlParams.get("postinterval") || session.postInterval; session.postInterval = parseInt(session.postInterval) || 60; if (session.postInterval < 5) { session.postInterval = 5; } } if (urlParams.has("postimage")) { var postURL = decodeURIComponent(urlParams.get("postimage")) || session.postURL; // default will post to https://temp.vdo.ninja/images/STREAMIDHERE.jpg , at an interval. it will be cached unless using url params. setInterval( function (postURL) { try { uploadImageSnapshot(postURL); } catch (e) {} }, session.postInterval * 1000, postURL ); } if (urlParams.has("cleanish")) { session.cleanish = true; } if (session.cleanish || !session.cleanOutput) { if (session.obsControls) { getById("obscontrolbutton").classList.remove("hidden"); getById("controlButtons").classList.remove("hidden"); } } if (urlParams.has("nocontrolbar")) { getById("controlButtons").classList.add("hidden"); getById("controlButtons").style.display = "none"; session.dedicatedControlBarSpace = false; } if (urlParams.has("channels")) { // must be loaded before channelOffset session.audioChannels = parseInt(urlParams.get("channels")) || 8; // for audio output ; not input. see: &channelcount instead. session.offsetChannel = 0; log("max channels is 32; channels offset"); session.audioEffects = true; } if (urlParams.has("channeloffset")) { session.offsetChannel = parseInt(urlParams.get("channeloffset")); log("max channels is 32; channels offset"); session.audioEffects = true; } if (urlParams.get("playchannel")) { // must be loaded before channelOffset session.playChannel = parseInt(urlParams.get("playchannel")); // for audio output ; not input. see: &channelcount instead. session.audioEffects = true; } if (urlParams.has("enhance")) { //if (parseInt(urlParams.get('enhance')>0){ session.enhance = true; //parseInt(urlParams.get('enhance')); //} } if (urlParams.has("degrade")) { session.degrade = urlParams.get("degrade") || true; // Firefox, and maybe Safari, supported I think. // the possible values are maintain-framerate, maintain-resolution, or balanced. The default value is balanced } if (urlParams.has("maxviewers") || urlParams.has("mv")) { session.maxviewers = urlParams.get("maxviewers") || urlParams.get("mv"); if (session.maxviewers.length == 0) { session.maxviewers = 1; } else { session.maxviewers = parseInt(session.maxviewers); } log("maxviewers set"); } if (urlParams.has("maxpublishers") || urlParams.has("mp")) { session.maxpublishers = urlParams.get("maxpublishers") || urlParams.get("mp"); if (session.maxpublishers.length == 0) { session.maxpublishers = 1; } else { session.maxpublishers = parseInt(session.maxpublishers); } log("maxpublishers set"); } if (urlParams.has("maxconnections") || urlParams.has("mc")) { session.maxconnections = urlParams.get("maxconnections") || urlParams.get("maxconnections"); if (session.maxconnections.length == 0) { session.maxconnections = 1; } else { session.maxconnections = parseInt(session.maxconnections); } log("maxconnections set"); } if (urlParams.has("secure")) { session.security = true; if (!session.cleanOutput) { delayedStartupFuncs.push([warnUser, "Enhanced Security Mode Enabled."]); } } if (urlParams.has("requireencryption")) { session.requireencryption = true; } if (urlParams.has("unsafe")) { session.unsafe = true; } if (urlParams.has("random") || urlParams.has("randomize")) { session.randomize = true; } 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); log("frameRate Changed"); log(session.frameRate); } if (urlParams.has("tz")) { // being depreciated, but still works with meshcast (no longer turn) session.tz = urlParams.get("tz"); if (session.tz === null || session.tz === "") { session.tz = false; } else { session.tz = parseInt(session.tz); } } if (urlParams.has("maxframerate") || urlParams.has("mfr") || urlParams.has("mfps")) { session.maxframeRate = urlParams.get("maxframerate") || urlParams.get("mfr") || urlParams.get("mfps"); session.maxframeRate = parseInt(session.maxframeRate); log("max frameRate assigned"); log(session.maxframeRate); } if (urlParams.has("buffer") || urlParams.has("buffer2")) { // needs to be before sync if (ChromiumVersion > 50 && ChromiumVersion < 78) { } else { session.buffer = parseFloat(urlParams.get("buffer")) || parseFloat(urlParams.get("buffer2")) || 0; log("buffer Changed: " + session.buffer); } if (urlParams.has("buffer2")) { session.includeRTT = true; } } if (urlParams.has("panning") || urlParams.has("pan")) { session.panning = urlParams.get("panning") || urlParams.get("pan"); if (session.panning === "") { session.panning = true; } session.audioEffects = true; } if (urlParams.has("sync")) { if (ChromiumVersion > 50 && ChromiumVersion < 78) { } else { 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; } } } if (urlParams.has("nomirror")) { session.nomirror = true; } if (urlParams.has("mirror")) { if (urlParams.get("mirror") == "3") { getById("main").classList.add("mirror"); } else if (urlParams.get("mirror") == "2") { session.mirrored = 2; } else if (urlParams.get("mirror") == "0") { session.mirrored = 0; } else if (urlParams.get("mirror") == "false") { session.mirrored = 0; } else if (urlParams.get("mirror") == "off") { session.mirrored = 0; } else { session.mirrored = 1; } } if (urlParams.has("mirroroutput")) { let mirrorOutputParam = urlParams.get("mirroroutput"); let value = mirrorOutputParam ? mirrorOutputParam.toLowerCase() : ""; let enableMirrorOutput = !(value === "0" || value === "false" || value === "off"); session.mirrorOutput = enableMirrorOutput; if (session.mirrorOutput) { session.permaMirrored = true; } else if (session.permaMirrored) { session.permaMirrored = false; } } if (urlParams.has("flip")) { if (urlParams.get("flip") == "0") { session.flipped = false; } else if (urlParams.get("flip") == "false") { session.flipped = false; } else if (urlParams.get("flip") == "off") { session.flipped = false; } else { session.flipped = true; } } if (urlParams.has("flipoutput")) { let flipOutputParam = urlParams.get("flipoutput"); let value = flipOutputParam ? flipOutputParam.toLowerCase() : ""; session.flipOutput = !(value === "0" || value === "false" || value === "off"); if (session.flipOutput) { session.flipped = true; } } 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 try { log("Mirror all videos"); var mirrorStyle = document.createElement("style"); mirrorStyle.innerHTML = "video {transform: scaleX(-1);}"; document.getElementsByTagName("head")[0].appendChild(mirrorStyle); } 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); } } if (urlParams.has("icefilter")) { log("ICE FILTER ENABLED"); session.icefilter = urlParams.get("icefilter"); } if (urlParams.has("lanonly")) { session.localNetworkOnly = true; session.configuration = { sdpSemantics: session.sdpSemantics // future-proofing }; } if (urlParams.has("stunonly")) { session.stunOnly = true; } // IPv6 handling: By default, prefer IPv4 over IPv6 when both are available. // This helps on "half-broken" IPv6 networks where the IPv6 path is flaky. // - &ipv6=0 (or &preferipv4): Disable IPv6 candidates if IPv4 exists (fallback to IPv6 if no IPv4) // - &ipv6=1: Allow normal IPv6/IPv4 behavior (both used equally) // - Default (no param): Prefer IPv4 by sending IPv4 candidates first, but still allow IPv6 if (urlParams.has("ipv6")) { var ipv6Value = urlParams.get("ipv6"); if (ipv6Value === "0" || ipv6Value === "false") { log("IPv6 disabled (will use IPv4 when available, fallback to IPv6 if needed)"); session.disableIpv6 = true; } else if (ipv6Value === "1" || ipv6Value === "true") { log("IPv6 explicitly enabled (normal dual-stack behavior)"); session.disableIpv6 = false; session.preferIpv4 = false; } } else if (urlParams.has("preferipv4") || urlParams.has("ipv4")) { log("IPv4 preferred: IPv6 candidates will be dropped if IPv4 exists"); session.disableIpv6 = true; } // Note: By default, session.preferIpv4 is true (set in webrtc.js defaults) // which reorders candidates to send IPv4 first but still allows IPv6. if (urlParams.has("activespeaker") || urlParams.has("speakerview") || urlParams.has("sas")) { session.activeSpeaker = urlParams.get("activespeaker") || urlParams.get("speakerview") || urlParams.get("sas") || 1; session.activeSpeaker = parseInt(session.activeSpeaker); session.style = 6; session.audioEffects = true; //session.audioMeterGuest = true; session.minipreview = 2; if (session.activeSpeaker == 1 || session.activeSpeaker == 3) { session.animatedMoves = false; } session.fadein = true; document.querySelector(":root").style.setProperty("--fadein-speed", 0.5); session.activeSpeakerInterval = setInterval(function () { activeSpeaker(false); }, 100); } else if (urlParams.has("noisegate") || urlParams.has("gating") || urlParams.has("gate") || urlParams.has("ng")) { session.quietOthers = urlParams.get("noisegate") || urlParams.get("gating") || urlParams.get("gate") || urlParams.get("ng") || 1; session.quietOthers = parseInt(session.quietOthers); if (session.quietOthers == 1) { session.quietOthers = false; session.noisegate = true; session.audioEffects = true; //session.audioMeterGuest = true; } else if (session.quietOthers == 4) { session.quietOthers = 1; session.audioEffects = true; //session.audioMeterGuest = true; session.activeSpeakerInterval = setInterval(function () { activeSpeaker(false); }, 100); } else if (!session.quietOthers) { session.noisegate = false; session.quietOthers = false; } else { session.audioEffects = true; //session.audioMeterGuest = true; session.activeSpeakerInterval = setInterval(function () { activeSpeaker(false); }, 100); } } if (urlParams.has("activespeakerdelay") || urlParams.has("speakerviewdelay") || urlParams.has("sasdelay")) { session.activeSpeakerTimeout = urlParams.get("activespeakerdelay") || urlParams.get("speakerviewdelay") || urlParams.get("sasdelay") || 0; session.activeSpeakerTimeout = parseInt(session.activeSpeakerTimeout); } if (urlParams.has("noisegatesettings")) { session.noisegateSettings = urlParams.get("noisegatesettings"); session.noisegateSettings = session.noisegateSettings.split(","); } if (urlParams.has("fadein")) { session.fadein = true; if (urlParams.get("fadein") || 0) { try { var fadeinspeed = parseInt(urlParams.get("fadein") || 0) / 1000.0; fadeinspeed += "s"; document.querySelector(":root").style.setProperty("--fadein-speed", fadeinspeed); } catch (e) { errorlog("variable css failed"); } } else { try { var fadeinspeed = 0.5; fadeinspeed += "s"; document.querySelector(":root").style.setProperty("--fadein-speed", fadeinspeed); } catch (e) { errorlog("variable css failed"); } } } if (urlParams.has("widget")) { session.widget = urlParams.get("widget") || false; if (session.widget === "false" || session.widget === "0" || session.widget === "off") { session.noWidget = true; session.widget = false; } else if (session.widget) { session.widget = decodeURI(session.widget) || false; log(session.widget); } } if (urlParams.has("widgetleft")) { session.widgetleft = true; } if (urlParams.get("widgetwidth")) { // default is 25% try{ session.widgetwidth = parseFloat(urlParams.get("widgetwidth")) || 25; if (session.widgetwidth>50){ session.widgetwidth = 50; } document.querySelector(":root").style.setProperty("--widget-width", session.widgetwidth+"%"); } catch(e){ errorlog(e); } } if (urlParams.has("animated") || urlParams.has("animate")) { session.animatedMoves = urlParams.get("animated") || urlParams.get("animate"); if (session.animatedMoves === "false") { session.animatedMoves = false; } else if (session.animatedMoves === "0") { session.animatedMoves = false; } else if (session.animatedMoves === "no") { session.animatedMoves = false; } else if (session.animatedMoves === "off") { session.animatedMoves = false; } else { session.animatedMoves = parseInt(session.animatedMoves) || 100; } if (session.animatedMoves > 200) { session.animatedMoves = 200; } } else if (session.mobile) { session.animatedMoves = false; } if (urlParams.has("meter") || urlParams.has("meterstyle")) { // same as also adding &style=3 session.meterStyle = urlParams.get("meter") || urlParams.get("meterstyle") || 1; session.meterStyle = parseInt(session.meterStyle); if (session.meterStyle < 4) { session.style = 3; // black canvas } else { session.style = -1; // no canvas } session.audioEffects = true; } if (session.meterStyle == 5) { document.documentElement.style.setProperty("--video-background-image-size-talking", "auto 35%"); document.documentElement.style.setProperty("--video-background-image-size-screaming", "auto 45%"); } if (urlParams.has("directorchat") || urlParams.has("dc")) { session.directorChat = true; } if (urlParams.has("style") || urlParams.has("st")) { session.style = urlParams.get("style") || urlParams.get("st"); if (parseInt(session.style) === 0 || session.style == "controls") { // no audio only session.style = 0; } else 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 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") { // audio meter ; see &meterstyle , where optios include default(false), 1, and 2. session.style = 3; session.audioEffects = true; } else if (parseInt(session.style) == 4) { // black background session.style = 4; } else if (parseInt(session.style) == 5) { // random colored background session.style = 5; } else if (parseInt(session.style) == 7) { // shows video elements for all connections; even those without video/audio session.style = parseInt(session.style); session.showall = true; } else if (parseInt(session.style)) { // 6 is the first letter of the name, surrounded with a colored circle session.style = parseInt(session.style); } else { session.style = 1; } } //if (session.style){ // getById("toggleWaveformButton").classList.remove("hidden"); //} if (urlParams.has("showall")) { // just an alternative; might be compoundable session.showall = true; } if (urlParams.has("samplerate") || urlParams.has("sr")) { // playout sample rate session.sampleRate = parseInt(urlParams.get("samplerate")) || parseInt(urlParams.get("samplerate")) || 48000; 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 // default is 48000 already }); session.audioEffects = true; } if (session.audioCodec === "lyra") { // WIP. does not work try { var { default: Module } = await import("./thirdparty/lyra/webassembly_codec_wrapper.js"); await Module() .then(module => { console.log("Initialized codec's wasmModule."); session.lyraCodecModule = module; }) .catch(e => { console.log(`Module() error: ${e.name} message: ${e.message}`); }); } catch (e) { errorlog(e); } if (session.lyraCodecModule) { console.log("Lyra module loaded"); session.micSampleRate = 16000; session.encodedInsertableStreams = "lyra"; } else { console.log("Lyra module failed to load"); } } if (urlParams.has("e2ee")) { session.encodedInsertableStreams = "e2ee"; } else if (urlParams.has("insertablestreams") || urlParams.has("is")) { session.encodedInsertableStreams = urlParams.get("insertablestreams") || urlParams.get("is") || true; } if (urlParams.has("outboundsamplerate") || urlParams.has("obsr")) { session.outboundSampleRate = parseInt(urlParams.get("outboundsamplerate")) || parseInt(urlParams.get("obsr")) || false; // default null } else { session.outboundSampleRate = null; // tmp } if (urlParams.has("micsamplerate") || urlParams.has("msr")) { session.micSampleRate = parseInt(urlParams.get("micsamplerate")) || parseInt(urlParams.get("msr")) || 48000; } if (urlParams.has("micsamplesize")) { session.micSampleSize = parseInt(urlParams.get("micsamplesize")) || 16; } if (urlParams.has("noaudioprocessing") || urlParams.has("noap")) { session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? session.disableViewerWebAudioPipeline = true; // this has the potential to break things. session.audioEffects = false; // disable audio inbound effects also. session.audioMeterGuest = false; if (session.noisegate === null) { session.noisegate = false; } } // For info, see this: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/availableOutgoingBitrate if (urlParams.has("maxbandwidth")) { // limits the bitrate based on the outbound total available bandwidth; chromium-based session.maxBandwidth = urlParams.get("maxbandwidth") || 80; // 0 to 100; will reduce bitrate as a percentage of available session.maxBandwidth = parseInt(session.maxBandwidth); if (session.maxBandwidth > 200) { // will over ride default 2500kbps if no bitrate is specified session.maxBandwidth = 200; } else if (session.maxBandwidth < 0) { session.maxBandwidth = 0; } } if (urlParams.has("iframetarget")) { session.iframetarget = urlParams.get("iframetarget"); // speciifies the IFRAME Hostname target if (session.iframetarget) { session.iframetarget = decodeURIComponent(session.iframetarget); } else { session.iframetarget = (window.location.protocol === "file:") ? "*" : window.location.origin; } } if (urlParams.has("sendframes")) { session.sendframes = urlParams.get("sendframes"); if (session.sendframes) { try { session.sendframes = decodeURIComponent(session.sendframes); } catch (e) {} } else { session.sendframes = session.iframetarget || "*"; } } if (urlParams.has("tcp")) { // forces the TURN servers to use TCP mode; still need to add &private to force TURN also tho session.forceTcpMode = true; } if (urlParams.has("stun")) { var stunstring = urlParams.get("stun"); stunstring = stunstring.split(";"); if (stunstring[0] !== "false") { // false disables the TURN server. Useful for debuggin var stun = {}; if (stunstring.length == 3) { stun.username = stunstring[0]; // myusername stun.credential = stunstring[1]; //mypassword stun.urls = [stunstring[2]]; // ["turn:turn.obs.ninja:443"]; } else if (stunstring.length == 1) { stun.urls = [stunstring[0]]; } session.stunServers = [stun]; } else { session.stunServers = []; } } if (urlParams.has("addstun")) { var stunstring = urlParams.get("addstun"); stunstring = stunstring.split(";"); var stun = {}; if (stunstring.length == 3) { stun.username = stunstring[0]; // myusername stun.credential = stunstring[1]; //mypassword stun.urls = [stunstring[2]]; // ["turn:turn.obs.ninja:443"]; } else if (stunstring.length == 1) { stun.urls = [stunstring[0]]; } session.stunServers = session.stunServers.concat(stun); } if (urlParams.has("bundle")) { session.bundlePolicy = urlParams.get("bundle") || "max-bundle"; // default is browser default. } if (urlParams.has("planb")) { session.sdpSemantics = "plan-b"; // for legacy support, or debuggin, or whatever. } if (urlParams.has("turn")) { var turnstring = urlParams.get("turn"); if (turnstring == "twilio") { // a sample function on loading remote credentials for TURN servers. try { session.ws = false; // prevents connection var twillioRequest = new XMLHttpRequest(); twillioRequest.onload = function () { if (this.status === 200) { try { var res = JSON.parse(this.responseText); } catch (e) { console.error(e); return; } 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" } ], sdpSemantics: session.sdpSemantics // future-proofing }; if (session.ws === false) { session.ws = null; // allows connection (clears state) session.connect(); // connect if not already connected. } } // system does not connect if twilio API does not respond. }; twillioRequest.open("GET", "https://turn.example.com:443/twilio", true); // `false` makes the request synchronous twillioRequest.send(); } catch (e) { errorlog("Twilio Failed"); } } else if (turnstring == "nostun") { // disable TURN servers session.configuration = { sdpSemantics: session.sdpSemantics // future-proofing }; } else if (turnstring == "false" || turnstring == "off" || turnstring == "0") { // disable TURN servers session.configuration = { iceServers: session.stunServers, sdpSemantics: session.sdpSemantics // future-proofing }; } else { try { //session.configuration = {iceServers: [], sdpSemantics: session.sdpSemantics}; turnstring = turnstring.split(";"); if (turnstring !== "false") { // false disables the TURN server. Useful for debuggin var turn = {}; if (turnstring.length == 3) { turn.username = turnstring[0]; // myusername turn.credential = turnstring[1]; //mypassword turn.urls = [turnstring[2]]; // ["turn:turn.obs.ninja:443"]; } else if (turnstring.length == 1) { turn.urls = [turnstring[0]]; } session.configuration = { iceServers: session.stunServers, sdpSemantics: session.sdpSemantics // future-proofing }; session.configuration.iceServers.push(turn); } } catch (e) { if (!session.cleanOutput) { warnUser("TURN server parameters were wrong."); } errorlog(e); } } } if (urlParams.has("apiserver") && urlParams.get("apiserver")) { // must set this after any custom TURN / STUN settings, else it might over-ride them. session.apiserver = urlParams.get("apiserver"); } if (urlParams.has("speedtest")) { // must set this after any custom TURN / STUN settings, else it might over-ride them. session.speedtest = true; if (urlParams.get("speedtest")) { // forces essentially UDP mode, unless TCP is specified, and some other stuff session.speedtest = urlParams.get("speedtest").toLowerCase(); // also limits bitrate } setupSpeedtest(); } if (urlParams.has("privacy") || urlParams.has("private") || urlParams.has("relay")) { // please only use if you are also using your own TURN service. session.privacy = urlParams.get("privacy") || urlParams.get("private") || urlParams.get("relay") || true; try { // I'll re-apply this in the setupSpeedtest() promise callback, just to be case. if (session.configuration) { // this needs to set last, otherwise it might be overridden session.configuration.iceTransportPolicy = "relay"; // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/address } } catch (e) { if (!session.cleanOutput) { warnUser("Privacy mode failed to configure."); } errorlog(e); } if (session.speedtest) { warnlog("Bitrate being throttled to max of 6000 kbps"); if (session.maxvideobitrate !== false) { if (session.maxvideobitrate > 6000) { session.maxvideobitrate = 6000; // Please feel free to get rid of this if using your own TURN servers... } } else { session.maxvideobitrate = 6000; // don't let people pull more than 6000 from you } if (session.bitrate !== false) { if (session.bitrate > 6000) { session.bitrate = 6000; // Please feel free to get rid of this if using your own TURN servers... } } } else { warnlog("Bitrate being throttled to max of 4000 kbps"); if (session.maxvideobitrate !== false) { if (session.maxvideobitrate > 4000) { session.maxvideobitrate = 4000; // Please feel free to get rid of this if using your own TURN servers... } } else { session.maxvideobitrate = 4000; // don't let people pull more than 4000 from you } if (session.bitrate !== false) { if (session.bitrate > 4000) { session.bitrate = 4000; // Please feel free to get rid of this if using your own TURN servers... } } } } if (urlParams.has("osc") || urlParams.has("api")) { if (urlParams.get("osc") || urlParams.get("api")) { session.api = urlParams.get("osc") || urlParams.get("api") || false; if (session.api) { setTimeout(function () { oscClient(); }, 1000); } } } if (urlParams.has("postapi") || urlParams.has("posturl")) { session.postApi = urlParams.get("postapi") || urlParams.get("posturl") || false; // ie: &postapi=https%3A%2F%2Fwebhook.site%2Fb190f5bf-e4f8-454a-bd51-78b5807df9c1 if (session.postApi) { try { session.postApi = decodeURI(session.postApi) || session.postApi; // needs to be SSL enabled. } catch (e) { console.error(e); } } } if (urlParams.has("queue")) { session.queue = true; if (urlParams.get("queue") === "false") { session.queue = false; } else if (urlParams.get("queue") === "0") { session.queue = false; } else if (urlParams.get("queue") === "off") { session.queue = false; } else if (urlParams.get("queue")) { session.queue = urlParams.get("queue"); } } if (urlParams.has("queue2") || urlParams.has("screen")) { // the guest can see the director, if the director doesn't have &queue session.queue = true; session.queueType = 2; } if (urlParams.has("queue3") || urlParams.has("hold")) { // &hold (alias: &queue3) - Full bidirectional isolation until activated. // // - Guest cannot see director or other guests // - Director cannot see guest's video/audio (only control box with label) // - Other guests cannot see the hold guest // - On activation, all directions open and normal flow resumes // // Technical: Sets needsPublishing=true, skips initialPublish until activated. // Use case: Green room / screening where director doesn't want to be seen either. session.queue = true; session.queueType = 3; } if (urlParams.has("queue4") || urlParams.has("holdwithvideo")) { // &holdwithvideo (alias: &queue4) - Like &hold but allows Guest→Director media. // // - Guest cannot see director or other guests (still isolated) // - Director CAN see guest's video/audio (for preview/screening) // - Other guests cannot see the hold guest // - On activation, remaining directions open // // IMPORTANT: The name "holdwithvideo" is slightly misleading. It does NOT force // video to be sent. It simply removes the publishing block that &hold creates. // The actual video/audio that flows is still determined by: // - What the director requests ({video: true/false, audio: true/false}) // - Room-level rules (&novideo, &nodirectorvideo, etc.) // - All normal gating logic // // Technical: Calls initialPublish normally (unlike queue3), respects allowVideo/allowAudio. // Use case: Director wants to preview guest (check lighting, verify identity) before admission. session.queue = true; session.queueType = 4; } if (session.director && (urlParams.has("approvepopup") || urlParams.has("approvalpopup"))) { // Opt-in approval popup for directors session.approval_popup = true; try { log("[flags] &approvepopup detected; approval_popup=true"); } catch (e) {} } // do not reference stream ID before this point, as it might change after this point. if (urlParams.has("push") || urlParams.has("id") || urlParams.has("permaid") || (session.sticky && session.decrypted)) { session.permaid = urlParams.get("push") || urlParams.get("id") || urlParams.get("permaid"); if (session.permaid) { session.permaid = sanitizeStreamID(session.permaid) || null; session.streamID = session.permaid || session.streamID; } else if ((urlParams.has("permaid") || (session.sticky && session.decrypted)) && getStorage("permaid")) { session.streamID = sanitizeStreamID(getStorage("permaid")) || session.streamID; session.permaid = null; } else { session.permaid = null; } if (session.permaid && ((session.permaid.length<3) || (session.permaid==="test"))) { if (session.password === session.defaultPassword) { if (location.hostname === "vdo.ninja") { if (!session.cleanOutput){ window.focus(); warnUser(getTranslation("insecure-stream-id"),10000); } } } } if (urlParams.has("permaid") || (session.sticky && session.decrypted)) { setStorage("permaid", session.streamID, 99999); } if (urlParams.has("push")) { updateURL("push=" + session.streamID, true, false); } else if (urlParams.has("id")) { updateURL("id=" + session.streamID, true, false); // not 'officially' supporting this yet; we'll see. } else if (urlParams.has("permaid")) { updateURL("permaid=" + session.streamID, true, false); } else { updateURL("push=" + session.streamID, true, false); } if (session.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); session.permaid = false; // used to avoid a trigger later on. } else { getById("container-1").className = "column columnfade hidden"; getById("container-4").className = "column columnfade hidden"; getById("dropButton").className = "column columnfade hidden"; getById("info").innerHTML = ""; if (session.videoDevice === 0) { miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); } else { miniTranslate(getById("add_camera"), "share-your-camera", "Share your Camera"); } miniTranslate(getById("add_screen"), "share-your-screen", "Share your Screen"); getById("container-2").title = getById("add_screen").innerText; getById("container-3").title = getById("add_camera").innerText; getById("passwordRoom").value = ""; getById("videoname1").value = ""; getById("dirroomid").innerHTML = ""; getById("roomid").innerHTML = ""; 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. //} if (session.webcamonly == true) { // mobile or manual flag 'webcam' pflag set getById("head1").innerHTML = '- Please accept any camera permissions'; } else { getById("head1").innerHTML = '
- Please select which you wish to share'; } if (!session.cleanOutput) { try { if (session.studioSoftware) { getById("unexpectedPushLink").classList.remove("hidden"); } } catch (e) {} } } } if (window.vdoAuth){ if (session.streamID) { await window.vdoAuth.assignStream(); } getById("mainmenu").classList.remove("hidden2"); getById("header").classList.remove("hidden2"); } if (session.roomid || urlParams.has("roomid") || urlParams.has("r") || urlParams.has("room") || filename || session.permaid !== false) { var roomid = ""; if (urlParams.has("room")) { // needs to be first; takes priority 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; } else if (filename) { roomid = filename; } session.roomid = sanitizeRoomName(roomid); if (session.director) { if (session.director !== session.roomid) { if (!session.cleanOutput) { warnUser("Conflicting director and room values were provided.\n\n Check your URL parameters; there should be only &director OR &room", 5000); } } session.roomid = false; } if (session.quality===false){ try { document.getElementById("webcamquality").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : (session.quality_wb || 0)); document.getElementById("webcamquality3").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : (session.quality_wb || 0)); } catch(e){} } } else if (session.quality===false){ try { document.getElementById("webcamquality").elements.namedItem("resolution").value = session.quality_wb || 0; document.getElementById("webcamquality3").elements.namedItem("resolution").value = session.quality_wb || 0; } catch(e){} } if (session.permaid === false && session.roomid === false && session.view === false && session.effect === false && session.director === false) { session.effect = null; getById("credits").innerHTML = "Version: " + session.version + ' | ' + getById("credits").innerHTML; } if (session.mobile && session.permaid === false && !session.roomid) { getById("rememberStreamID").classList.remove("hidden"); let rememberStreamIDmobile = getStorage("rememberStreamIDmobile"); if (rememberStreamIDmobile === "false") { getById("rememberStreamIDcheck").checked = false; } } if (urlParams.has("hostwhep") || urlParams.has("whepout")) { session.whepHost = urlParams.get("hostwhep") || urlParams.get("whepout") || session.streamID || false; } // General WHIP output toggles if (urlParams.has("whipnoscreen")) { session.whipPublishScreen = false; } if (urlParams.has("whipprimaryonly")) { session.whipPublishPrimary = true; session.whipPublishScreen = false; } if (urlParams.has("whipscreenonly")) { session.whipPublishPrimary = false; session.whipPublishScreen = true; } if (urlParams.get("mediamtx")){ session.mediamtx = urlParams.get("mediamtx"); } if (session.mediamtx){ if (urlParams.has("mediamtxnoscreen")) { session.whipPublishScreen = false; } if (urlParams.has("mediamtxscreenonly")) { session.whipPublishPrimary = false; session.whipPublishScreen = true; } if (!session.mediamtx.includes(".") && !session.mediamtx.includes("localhost")){ session.mediamtx += ".com"; } if (!session.mediamtx.includes(":")){ session.mediamtx += ":8889"; } let mediamtxBase = session.mediamtx; let scheme = "https://"; if (mediamtxBase.startsWith("http://") || mediamtxBase.startsWith("https://")) { scheme = ""; } else if (mediamtxBase.startsWith("localhost:")) { scheme = "http://"; } if (scheme){ mediamtxBase = scheme + mediamtxBase; } if (mediamtxBase.endsWith("/")) { mediamtxBase = mediamtxBase.slice(0, -1); } const streamId = session.streamID; const screenId = streamId + "_s"; const buildEndpoint = id => `${mediamtxBase}/${id}`; const primaryWhipUrl = `${buildEndpoint(streamId)}/whip`; const primaryWhepSettings = { type: "whep", url: `${buildEndpoint(streamId)}/whep`, token: streamId, media: "primary", started: false }; const screenWhipUrl = `${buildEndpoint(screenId)}/whip`; const screenWhepSettings = { type: "whep", url: `${buildEndpoint(screenId)}/whep`, token: screenId, media: "screen", started: false }; if (session.whipPublishPrimary) { if (!session.whipOutputUserSet) { session.whipOutput = primaryWhipUrl; } if (!session.whipoutSettingsUserSet) { session.whipoutSettings = primaryWhepSettings; } } else { if (!session.whipOutputUserSet) { session.whipOutput = false; } if (!session.whipoutSettingsUserSet) { session.whipoutSettings = false; } } if (session.whipPublishScreen) { if (!session.whipOutputScreenUserSet) { session.whipOutputScreen = screenWhipUrl; } if (!session.whipoutScreenSettingsUserSet) { session.whipoutScreenSettings = screenWhepSettings; } } else { if (!session.whipOutputScreenUserSet) { session.whipOutputScreen = false; } if (!session.whipoutScreenSettingsUserSet) { session.whipoutScreenSettings = false; } } if (session.whipPublishPrimary && session.whipoutSettings) { console.log("WHIP OUT: " + session.whipOutput + ", WHEP SHARE: " + session.whipoutSettings.url); } if (session.whipPublishScreen && session.whipoutScreenSettings) { console.log("WHIP OUT SCREEN: " + session.whipOutputScreen + ", WHEP SHARE: " + session.whipoutScreenSettings.url); } if (session.whipPublishScreen && session.whipOutputScreen && session.screenShareState) { whipOutScreen(); } if (session.stereo === false){ if (!session.whipOutAudioCodec || (session.whipOutAudioCodec=="opus")){ session.stereo=3; } } } if (urlParams.has("meshcastnoscreen")) { session.whipPublishScreen = false; } if (urlParams.has("meshcastscreenonly")) { session.whipPublishPrimary = false; session.whipPublishScreen = true; } if (urlParams.has("meshcastprimaryonly")) { session.whipPublishPrimary = true; session.whipPublishScreen = false; } if (urlParams.has("effects") || urlParams.has("effect")) { session.effect = urlParams.get("effects") || urlParams.get("effect") || null; } else if (urlParams.has("digitalzoom")) { session.effect = "7"; if (urlParams.get("digitalzoom")){ session.effectValue_default = parseFloat(urlParams.get("digitalzoom")) || 1; } } if (session.effect && !session.cleanOutput) { if (ChromiumVersion && ChromiumVersion === 122) { warnUser("⚠️ Notice: A recent update to Chrome/Edge can cause the browser to crash, especially when using &effects or &zoom.\n\nBrowser updates are rolling out to fix the issue, however avoiding the use of digital video effects for now might be prudent", 30000); } } if (urlParams.get("zoom")){ session.zoom = parseFloat(urlParams.get("zoom")) || false; session.ptz = true; } if (urlParams.get("wb") || urlParams.get("whitebalance")) { session.whiteBalance = urlParams.get("wb") || urlParams.get("whitebalance"); } if (urlParams.get("exposure")) { session.exposure = urlParams.get("exposure"); } if (urlParams.get("saturation")) { session.saturation = urlParams.get("saturation"); } if (urlParams.get("sharpness")) { session.sharpness = urlParams.get("sharpness"); } if (urlParams.get("contrast")) { session.contrast = urlParams.get("contrast"); } if (urlParams.get("brightness")) { session.brightness = urlParams.get("brightness"); } if (urlParams.get("focus")) { session.focusDistance = urlParams.get("focus"); } if (urlParams.has("wss")) { session.customWSS = true; session.wssSetViaUrl = true; if (urlParams.get("wss")) { session.wss = urlParams.get("wss"); if (!session.wss.startsWith("wss://")) { session.wss = "wss://" + session.wss; } } } else if (urlParams.has("wss2")) { session.wssSetViaUrl = true; if (urlParams.get("wss2")) { session.wss = urlParams.get("wss2"); if (!session.wss.startsWith("wss://")) { session.wss = "wss://" + session.wss; } } } else if (urlParams.get("audience")) { session.audience = urlParams.get("audience"); if (urlParams.get("audience") && session.view !== false) { session.wss = "wss://audience.vdo.ninja/listen/" + session.audience; } else { session.wss = "wss://audience.vdo.ninja/publish/" + session.audience; } } if (urlParams.has("bypass")) { session.bypass = true; if (!urlParams.get("bypass")){ session.customWSS = true; } } if (window.FaceDetector !== undefined) { document.querySelectorAll(".facetracker").forEach(ele => { ele.disabled = null; ele.removeAttribute("disabled"); ele.title = "Will slowly pan, tilt, and zoom in on the first face detected"; }); } if (urlParams.has("imagelist")) { // "&imagelist="+encodeURIComponent(JSON.stringify(["./media/bg_sample.webp", "./media/bg_sample2.webp"])) var imageList = urlParams.get("imagelist"); // if (imageList) { try { imageList = JSON.parse(decodeURIComponent(imageList)); if (imageList.length) { session.defaultBackgroundImages = imageList; // ["./media/bg_sample.webp", "./media/bg_sample2.webp"] } else { warnlog("empty image array; skipping"); } } catch (e) { console.error(e); try { imageList = decodeURIComponent(imageList); } catch (e) { console.error(e); } if (imageList) { session.defaultBackgroundImages = [imageList]; // ["./media/bg_sample.webp", "./media/bg_sample2.webp"] } else { warnlog("empty image array; skipping"); } } } } if (session.effect !== false) { if (session.effect === null) { getById("effectsDiv").style.display = "inline-block"; session.effect = "0"; } else if (session.effect === "0" || session.effect === "false" || session.effect === "off" || session.effect === 0) { session.effect = false; getById("effectSelector3").style.display = "none"; getById("effectsDiv3").style.display = "none"; getById("effectSelector").style.display = "none"; getById("effectsDiv").style.display = "none"; } if (session.effect === "5") { loadContentEffectsImages(); getById("effectSelector").style.display = "none"; getById("effectsDiv").style.display = "inline-block"; } if (session.effect === "3a") { // heavier blur session.effectValue = 5; session.effect = "3"; } else if (session.effect === "3") { session.effectValue = 2; } else if (session.effect === "7") { session.effectValue = session.effectValue || 1; } else if (["15", "14"].includes(session.effect)) { session.effectValue = 25; getById("effectSelector").style.display = "none"; getById("effectsDiv").style.display = "inline-block"; loadContentEffectsImages(); } // mirror == 2 // face == 1 // blur = 3 // green = 4 // image = 5 } if (urlParams.has("effectvalue") || urlParams.has("ev")) { session.effectValue = parseFloat(urlParams.get("effectvalue")) || parseFloat(urlParams.get("ev")) || 0; session.effectValue_default = session.effectValue; } if (session.webcamonly == true) { if (session.introButton) { getById("container-2").className = "column columnfade hidden"; // Hide screen share getById("head3").classList.add("hidden"); getById("head3a").classList.add("hidden"); } else { getById("container-2").className = "column columnfade hidden"; // Hide screen share getById("container-3").classList.add("skip-animation"); getById("container-3").classList.remove("pointer"); delayedStartupFuncs.push([previewWebcam]); } } if (session.introOnClean && session.permaid === false && session.roomid === false) { //getById("container-2").className = 'column columnfade hidden'; // Hide screen share getById("head3").classList.add("hidden"); getById("head3a").classList.add("hidden"); } else if (session.introOnClean && session.scene === false && (session.permaid !== false || session.roomid !== false)) { getById("container-2").className = "column columnfade hidden"; // Hide screen share getById("container-3").classList.add("skip-animation"); getById("container-3").classList.remove("pointer"); delayedStartupFuncs.push([previewWebcam]); } //if (!session.director && ((ChromiumVersion == 86) || (ChromiumVersion == 77) || (ChromiumVersion == 62) || (ChromiumVersion == 51)) && (((session.permaid===false) && session.view) || (session.scene!==false))){ // session.studioSoftware = true; // vmix if (session.cleanViewer) { if (((session.view!==false) || session.whepInput || session.whipView) && !session.director && session.permaid === false) { session.cleanOutput = true; } } if (urlParams.has("clock") || urlParams.has("clock24")) { let urlClock = urlParams.get("clock") || urlParams.get("clock24"); if (urlParams.has("clock24")) { session.clock24 = true; } session.showTime = true; if (urlClock === "false") { session.showTime = false; } else if (urlClock === "0") { session.showTime = false; } else if (urlClock === "1") { getById("overlayClockContainer2").classList.add("top"); getById("overlayClockContainer2").classList.add("left"); } else if (urlClock === "7") { getById("overlayClockContainer2").classList.add("bottom"); getById("overlayClockContainer2").classList.add("left"); } else if (urlClock === "4") { getById("overlayClockContainer2").classList.add("vmiddle"); getById("overlayClockContainer2").classList.add("left"); } else if (urlClock === "2") { getById("overlayClockContainer2").classList.add("top"); getById("overlayClockContainer2").classList.add("hmiddle"); } else if (urlClock === "8") { getById("overlayClockContainer2").classList.add("bottom"); getById("overlayClockContainer2").classList.add("hmiddle"); } else if (urlClock === "5") { getById("overlayClockContainer2").classList.add("vmiddle"); getById("overlayClockContainer2").classList.add("hmiddle"); } else if (urlClock === "3") { getById("overlayClockContainer2").classList.add("top"); getById("overlayClockContainer2").classList.add("right"); } else if (urlClock === "9") { getById("overlayClockContainer2").classList.add("bottom"); getById("overlayClockContainer2").classList.add("right"); } else if (urlClock === "6") { getById("overlayClockContainer2").classList.add("vmiddle"); getById("overlayClockContainer2").classList.add("right"); } } else if (session.cleanOutput) { session.showTime = false; } if (urlParams.has("timer")) { if (urlParams.get("timer") === "1") { getById("overlayClockContainer").classList.add("top"); getById("overlayClockContainer").classList.add("left"); } else if (urlParams.get("timer") === "7") { getById("overlayClockContainer").classList.add("bottom"); getById("overlayClockContainer").classList.add("left"); } else if (urlParams.get("timer") === "4") { getById("overlayClockContainer").classList.add("vmiddle"); getById("overlayClockContainer").classList.add("left"); } else if (urlParams.get("timer") === "2") { getById("overlayClockContainer").classList.add("top"); getById("overlayClockContainer").classList.add("hmiddle"); } else if (urlParams.get("timer") === "8") { getById("overlayClockContainer").classList.add("bottom"); getById("overlayClockContainer").classList.add("hmiddle"); } else if (urlParams.get("timer") === "5") { getById("overlayClockContainer").classList.add("vmiddle"); getById("overlayClockContainer").classList.add("hmiddle"); } else if (urlParams.get("timer") === "3") { getById("overlayClockContainer").classList.add("top"); getById("overlayClockContainer").classList.add("right"); } else if (urlParams.get("timer") === "9") { getById("overlayClockContainer").classList.add("bottom"); getById("overlayClockContainer").classList.add("right"); } else if (urlParams.get("timer") === "6") { getById("overlayClockContainer").classList.add("vmiddle"); getById("overlayClockContainer").classList.add("right"); } else { getById("overlayClockContainer").classList.add("top"); getById("overlayClockContainer").classList.add("hmiddle"); } } if (urlParams.has("miconlyoption") || urlParams.has("moo")) { session.optionalMicOnly = true; } if (urlParams.has("hidescreenshare") || urlParams.has("hidess") || urlParams.has("sshide") || urlParams.has("screensharehide")) { // this way I don't need to remember what it's called. I can just guess. :D session.screenShareElementHidden = true; } if (urlParams.has("sspaused") || urlParams.has("sspause") || urlParams.has("ssp")) { // this way I don't need to remember what it's called. I can just guess. :D session.screenShareStartPaused = true; } if (urlParams.has("zoomedbitrate") || urlParams.has("zb")) { // this way I don't need to remember what it's called. I can just guess. :D session.zoomedBitrate = urlParams.get("zoomedbitrate") || urlParams.get("zb") || 2500; session.zoomedBitrate = parseInt(session.zoomedBitrate); } 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); } else { session.screenshareid = session.streamID + "_ss"; } } if (urlParams.has("screensharevideoonly") || urlParams.has("ssvideoonly") || urlParams.has("ssvo")) { session.screenshareVideoOnly = true; getById("audioScreenShare1").classList.add("hidden"); } if (urlParams.has("screensharefps") || urlParams.has("ssfps")) { if (urlParams.get("screensharefps") || urlParams.get("ssfps")) { session.screensharefps = urlParams.get("screensharefps") || urlParams.get("ssfps"); session.screensharefps = parseInt(session.screensharefps) || 2; } } if (urlParams.has("dropdown")){ getById("dropButton").className = ""; } if (urlParams.has("screensharequality") || urlParams.has("ssq")) { session.screensharequality = urlParams.get("screensharequality") || urlParams.get("ssq") || "0"; if (session.screensharequality.toLowerCase() == "4k") { session.screensharequality = -2; } else if (session.screensharequality.toLowerCase() == "2160p") { session.screensharequality = -2; } else if (session.screensharequality.toLowerCase() == "2160") { session.screensharequality = -2; } else if (session.screensharequality.toLowerCase() == "2k") { session.screensharequality = -3; } else if (session.screensharequality.toLowerCase() == "1440p") { session.screensharequality = -3; } else if (session.screensharequality.toLowerCase() == "1440") { session.screensharequality = -3; } else if (session.screensharequality.toLowerCase() == "hd") { session.screensharequality = 1; } else if (session.screensharequality.toLowerCase() == "720p") { session.screensharequality = 1; } else if (session.screensharequality.toLowerCase() == "720") { session.screensharequality = 1; } else if (session.screensharequality.toLowerCase() == "fullhd") { session.screensharequality = 0; } else if (session.screensharequality.toLowerCase() == "1080p") { session.screensharequality = 0; } else if (session.screensharequality.toLowerCase() == "1080") { session.screensharequality = 0; } else if (session.screensharequality.toLowerCase() == "high") { session.screensharequality = 0; } else if (session.screensharequality.toLowerCase() == "360p") { session.screensharequality = 2; } else if (session.screensharequality.toLowerCase() == "360") { session.screensharequality = 2; } else if (session.screensharequality.toLowerCase() == "low") { session.screensharequality = 2; } else { session.screensharequality = parseInt(session.screensharequality) || 0; } try { getById("gear_screen").parentNode.removeChild(getById("gear_screen")); } catch (e) {} } if (urlParams.has("screensharebitrate") || urlParams.has("ssbitrate")) { session.screenShareBitrate = urlParams.get("screensharebitrate") || urlParams.get("ssbitrate"); session.screenShareBitrate = parseInt(session.screenShareBitrate) || 2500; } if (urlParams.has("compressed") || urlParams.has("compresssdp") || urlParams.has("compress")){ session.compressSDP = true; // WIP. } if (urlParams.has("screensharelabel") || urlParams.has("sslabel")) { session.screenShareLabel = urlParams.get("screensharelabel") || urlParams.get("sslabel"); try { session.screenShareLabel = decodeURIComponent(session.screenShareLabel); } catch (e) {} session.screenShareLabel = session.screenShareLabel.replace(/_/g, " "); } if (urlParams.has("whepshare") || urlParams.has("whepsrc")) { try { session.whepSrc = urlParams.get("whepshare") || urlParams.get("whepsrc") || null; log("WHEP SRC: " + session.whepSrc); if (session.whepSrc) { try { session.whepSrc = decodeURIComponent(session.whepSrc); } catch (e) { session.whepSrc = session.whepSrc; } } else { session.whepSrc = await promptAlt("Enter the WHEP source as a URL"); } if (session.whepSrc) { session.whipoutSettings = { type: "whep", url: session.whepSrc }; session.whipoutSettingsUserSet = true; } } catch (e) { errorlog(e); } } if (urlParams.has("whepsharetoken") || urlParams.has("whepsrctoken")) { if (session.whipoutSettings) { try { session.whepSrcToken = urlParams.get("whepsharetoken") || urlParams.get("whepsrctoken") || null; log("WHEP TOKEN: " + session.whepSrcToken); if (session.whepSrcToken) { try { session.whepSrcToken = decodeURIComponent(session.whepSrcToken); } catch (e) { session.whepSrcToken = session.whepSrcToken; } } else { session.whepSrcToken = await promptAlt("Enter the WHEP source token"); } if (session.whepSrcToken) { session.whipoutSettings.token = session.whepSrcToken; session.whipoutSettingsUserSet = true; } } catch (e) { errorlog(e); } } } if (session.roomid !== false) { if (!session.cleanOutput) { if (session.roomid === "test") { if (session.password === session.defaultPassword) { window.focus(); var testRoomResponse = confirm(getTranslation("room-test-not-good")); if (testRoomResponse == false) { hangup(); throw new Error("User requested to not enter room 'room'."); } } } } if (session.audioDevice === false && session.outputDevice === false) { getById("headphonesDiv2").classList.remove("hidden"); getById("headphonesDiv").classList.remove("hidden"); } getById("addPasswordBasic").style.display = "none"; getById("info").innerHTML = ""; getById("info").style.color = "#CCC"; getById("videoname1").value = session.roomid; getById("dirroomid").innerText = session.roomid; getById("roomid").innerText = session.roomid; getById("container-1").className = "column columnfade hidden"; getById("container-4").className = "column columnfade hidden"; // container 5 is share media file; 6 is share website getById("container-7").style.display = "none"; getById("container-8").style.display = "none"; getById("container-9").style.display = "none"; getById("container-10").style.display = "none"; getById("container-11").style.display = "none"; getById("container-12").style.display = "none"; getById("container-13").style.display = "none"; getById("container-14").style.display = "none"; getById("container-15").style.display = "none"; getById("container-16").style.display = "none"; getById("container-17").style.display = "none"; getById("container-18").style.display = "none"; getById("container-19").style.display = "none"; getById("container-20").style.display = "none"; getById("container-21").style.display = "none"; 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("head1").innerHTML = ""; } else { getById("head1").innerHTML = 'Please select an option to join.'; } if (session.roomid.length > 0) { if (session.videoDevice === 0) { if (session.audioDevice === 0) { miniTranslate(getById("add_camera"), "join-room", "Join Room"); } else { miniTranslate(getById("add_camera"), "join-room-with-mic", "Join Room with Microphone"); } } else if (session.audioDevice === 0) { miniTranslate(getById("add_camera"), "join-room-with-camera", "Join Room with Camera"); } else if (session.optionalMicOnly) { miniTranslate(getById("add_camera"), "join-room-with-video", "Join Room with Video"); miniTranslate(getById("add_microphone"), "join-room-with-mic-only", "Join Room with just Microphone"); getById("container-3a").classList.remove("hidden"); } else { miniTranslate(getById("add_camera"), "join-room-with-camera", "Join Room with Camera"); } miniTranslate(getById("add_screen"), "share-screen-with-room", "Screenshare with Room"); } else { if (session.videoDevice === 0) { miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); } else { miniTranslate(getById("add_camera"), "share-your-camera", "Share your Camera"); } miniTranslate(getById("add_screen"), "share-your-screen", "Share your Screen"); } getById("head3").classList.add("hidden"); getById("head3a").classList.add("hidden"); getById("container-2").title = getById("add_screen").innerText; getById("container-3").title = getById("add_camera").innerText; if (session.scene !== false) { getById("container-4").className = "column columnfade"; getById("container-3").className = "column columnfade"; getById("container-2").className = "column columnfade"; getById("container-1").className = "column columnfade"; getById("header").className = "hidden"; getById("info").className = "hidden"; getById("head1").className = "hidden"; getById("head2").className = "hidden"; getById("mainmenu").style.display = "none"; getById("translateButton").style.display = "none"; // getById("legal").style.display = "none"; log("Update Mixer Event on REsize SET"); window.onresize = updateMixer; window.onorientationchange = function () { setTimeout(updateMixer, 200); }; joinRoom(session.roomid); // this is a scene, so we want high resolutions getById("main").style.overflow = "hidden"; if (session.chatbutton === true) { getById("chatbutton").classList.remove("hidden"); getById("controlButtons").classList.remove("hidden"); } else if (session.chatbutton === false) { getById("chatbutton").classList.add("hidden"); } if (session.hangupbutton === true) { getById("controlButtons").classList.remove("hidden"); getById("hangupbutton").classList.remove("hidden"); getById("hangupbutton").className = "float"; } } else if (session.permaid === null && session.roomid == "") { if (!session.cleanOutput) { // getById("head3").classList.remove('hidden'); // getById("head3a").classList.remove('hidden'); } } else if (session.studioSoftware && !session.webcamonly && session.permaid === false && session.director === false && ((session.view!==false) || session.whepInput || session.whipView) && session.roomid.length > 0) { // we already know roomid !== false updateURL("scene", true, false); // we also know it's not a scene, but we will assume it is in this specific case. session.scene = 0; } else if (session.studioSoftware && !session.webcamonly && !session.cleanOutput && (session.permaid === false) && (session.director === false) && (session.view===false) && !session.whepInput && !session.whipView && (session.roomid.length > 0)) { try { getById("unexpectedPushLink").classList.remove("hidden"); } catch (e) {} } } else if (session.director) { // if I do a short form of this, it will cause duplications in the code elsewhere. if (directorLanding == false) { // implies director is not true or false, but a string try { var director_room_input = sanitizeRoomName(session.director); log("director_room_input:" + director_room_input); if (urlParams.has("codirector") || urlParams.has("directorpassword") || urlParams.has("dirpass") || urlParams.has("dp")) { session.directorPassword = urlParams.get("codirector") || urlParams.get("directorpassword") || urlParams.get("dirpass") || urlParams.get("dp"); if (!session.directorPassword) { window.focus(); session.directorPassword = await promptAlt(getTranslation("enter-director-password"), true); } else { try { session.directorPassword = decodeURIComponent(session.directorPassword); } catch (e) {} } if (session.directorPassword) { session.directorPassword = sanitizePassword(session.directorPassword); await generateHash(session.directorPassword + session.salt + "abc123", 12) .then(function (hash) { // million to one error. log("dir room hash is " + hash); session.directorHash = hash; return; }) .catch(errorlog); } else { session.directorPassword = false; } } setTimeout( function (director_room_input) { createRoom(director_room_input); }, 20, director_room_input ); } catch (e) { directorLanding = true; session.director = true; } } if (session.chatbutton === true) { getById("chatbutton").classList.remove("hidden"); getById("controlButtons").classList.remove("hidden"); } else if (session.chatbutton === false) { getById("chatbutton").classList.add("hidden"); } } else if (((session.view!==false) || session.whepInput || session.whipView) && session.permaid === false) { //if (!session.activeSpeaker){ session.audioMeterGuest = false; //} if (session.style === false && session.studioSoftware) { session.style = 1; } if (session.audioEffects === null) { session.audioEffects = false; } log("Update Mixer Event on REsize SET"); getById("translateButton").style.display = "none"; // getById("legal").style.display = "none"; window.onresize = updateMixer; window.onorientationchange = function () { setTimeout(function () { updateMixer(); }, 200); }; getById("main").style.overflow = "hidden"; if (session.chatbutton === true) { getById("chatbutton").classList.remove("hidden"); getById("controlButtons").classList.remove("hidden"); } else if (session.chatbutton === false) { getById("chatbutton").classList.add("hidden"); } if (session.hangupbutton === true) { getById("controlButtons").classList.remove("hidden"); getById("hangupbutton").classList.remove("hidden"); getById("hangupbutton").className = "float"; } } if (urlParams.has("nofileshare") || urlParams.has("nodownloads") || urlParams.has("nofiles")) { session.hostedFiles = false; session.nodownloads = true; getById("sharefilebutton").style.display = "none"; getById("sharefilebutton").classList.add("hidden"); } else if (urlParams.has("fileshare") || urlParams.has("fs")) { // } else if (session.mobile) { getById("sharefilebutton").style.display = "none"; getById("sharefilebutton").classList.add("hidden"); } else if (session.roomid == false) { getById("sharefilebutton").style.display = "none"; getById("sharefilebutton").classList.add("hidden"); } else if (session.scene !== false) { getById("sharefilebutton").style.display = "none"; getById("sharefilebutton").classList.add("hidden"); } else if (session.cleanOutput) { getById("sharefilebutton").style.display = "none"; getById("sharefilebutton").classList.add("hidden"); } if (session.audioEffects === null) { session.audioEffects = true; } if (session.audioEffects) { getById("channelGroup1").style.display = "block"; getById("channelGroup2").style.display = "block"; } 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("hideusermenu") || urlParams.has("nousermenu") || urlParams.has("hum")) { getById("advancedOptionsGeneral").classList.add("hidden"); } if ((session.view!==false) || session.whepInput || session.whipView) { getById("main").className = "main"; getById("credits").style.display = "none"; // getById("legal").style.display = "none"; try { if (session.label === false) { if (document.title == "") { document.title = "View=" + session.view.toString(); } else { document.title += ", View=" + session.view.toString(); } } } catch (e) { errorlog(e); } } if (urlParams.get("auth")) { session.auth = urlParams.get("auth"); } if (urlParams.has("waitimage")) { session.waitImage = urlParams.get("waitimage") || false; } if ((((session.view!==false) || session.whepInput || session.whipView) && session.roomid === false) || (session.waitImage && session.scene !== false)) { getById("container-4").className = "column columnfade"; getById("container-3").className = "column columnfade"; getById("container-2").className = "column columnfade"; getById("container-1").className = "column columnfade"; //getById("header").className = 'hidden'; getById("info").className = "hidden"; getById("header").className = "hidden"; getById("head1").className = "hidden"; getById("head2").className = "hidden"; getById("head3").classList.add("hidden"); getById("head3a").classList.add("hidden"); getById("mainmenu").style.backgroundRepeat = "no-repeat"; getById("mainmenu").style.backgroundPosition = "bottom center"; getById("mainmenu").style.minHeight = "100%"; getById("mainmenu").style.minWidth = "100%"; getById("mainmenu").style.backgroundSize = "100px 100px"; getById("mainmenu").innerHTML = ""; getById("mainmenu").classList.remove("row"); getById("mainmenu").style.display = "block"; if (urlParams.has("waittimeout")) { session.waitImageTimeout = parseInt(urlParams.get("waittimeout")) || 0; } if (!session.fakeFeeds) { session.waitImageTimeoutObject = setTimeout(function () { session.waitImageTimeoutObject = true; try { if ((session.view!==false) || session.whepInput || session.whipView) { if (document.getElementById("mainmenu")) { if (session.waitImage) { getById("mainmenu").innerHTML += ''; getById("retryimage").src = decodeURIComponent(session.waitImage); getById("retryimage").onerror = function () { this.style.display = "none"; }; if (session.cover) { getById("retryimage").style.objectFit = "cover"; } } else if (!session.cleanOutput) { getById("mainmenu").innerHTML += '
'; getById("retrySpinner").onclick = function () { updateURL("cleanoutput"); location.reload(); }; getById("retrySpinner").title = getTranslation("waiting-for-the-stream"); } if (urlParams.has("waitmessage")) { getById("mainmenu").innerHTML += '
'; getById("retrymessage").innerText = urlParams.get("waitmessage"); getById("retrySpinner").title = urlParams.get("waitmessage"); } } } } catch (e) { errorlog(e); } }, session.waitImageTimeout); } log("auto request videos"); if ((iPad || iOS) && navigator.userAgent.indexOf("Safari") != -1 && navigator.userAgent.indexOf("Chrome") == -1 && SafariVersion > 13) { // Modern iOS doesn't need pop up play(); } else if (navigator.userAgent.indexOf("Safari") != -1 && (navigator.userAgent.indexOf("Chrome") == -1 && navigator.userAgent.indexOf("Chromium") == -1)) { // Safari on Desktop does require pop up try { navigator.mediaDevices .getUserMedia({ audio: true }) .then(function () { closeModal(); play(); }) .catch(function () { play(); }); if (!session.cleanOutput) { warnUser("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", 20000); } } catch(e){ errorlog(e); play(); } } 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 { document.title += ": " + session.roomid.toString(); } } } catch (e) { errorlog(e); } } else { try { let reloadOldRoom = getStorage("directorOtherSettings"); if (reloadOldRoom && reloadOldRoom.roomid) { getById("lastSavedRoomName").innerText = reloadOldRoom.roomid; getById("lastSavedRoom").classList.remove("hidden"); getById("goToLastSavedRoom").onclick = function () { createRoom(false, true); }; } } catch (e) { errorlog(e); } } hideHomeCheck(); setTimeout(function () { for (var i in delayedStartupFuncs) { var cb = delayedStartupFuncs[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 } delayedStartupFuncs = []; }, 50); if (session.effect == "3" || session.effect == "4" || session.effect == "5") { attemptTFLiteJsFileLoad(); } else if (session.effect == "6") { loadTensorflowJS(); } else if (session.effect == "9") { session.effect = "sample"; //loadEffect(session.effect); warnUser("Loading custom effects model...", 1000); } else if (session.effect == "10") { session.effect = "dog"; //loadEffect(session.effect); warnUser("Loading custom effects model...", 1000); } else if (session.effect == "11") { session.effect = "anon"; //loadEffect(session.effect); warnUser("Loading custom effects model...", 1000); } else if (session.effect == "12") { session.effect = "sample"; //loadEffect(session.effect); warnUser("Loading custom effects model...", 1000); } else if (session.effect == "facetracking") { session.effect = "1"; } else if (session.effect == "zoom") { session.effect = "7"; } if (session.effect === "3") { getById("selectEffectAmount").style.display = "block"; getById("selectEffectAmount3").style.display = "block"; getById("selectEffectAmountInput").value = session.effectValue; getById("selectEffectAmountInput3").value = session.effectValue; } else if ((session.effect === "14" || session.effect === "15") && session.effectValue_default == false){ getById("selectEffectAmount").style.display = "block"; getById("selectEffectAmount3").style.display = "block"; if (session.effectValue_default) { session.effectValue = parseFloat(session.effectValue_default); } else { session.effectValue = parseFloat(session.effectValue); } getById("selectEffectAmountInput").min = 1; getById("selectEffectAmountInput").max = 50; getById("selectEffectAmountInput").step = 1; getById("selectEffectAmountInput3").min = 1; getById("selectEffectAmountInput3").max = 50; getById("selectEffectAmountInput3").step = 1; getById("selectEffectAmountInput").value = session.effectValue; getById("selectEffectAmountInput3").value = session.effectValue; } else if (session.effect === "7") { getById("selectEffectAmount").style.display = "block"; getById("selectEffectAmount3").style.display = "block"; if (session.effectValue_default) { session.effectValue = session.effectValue_default; } else { session.effectValue = 1; } getById("selectEffectAmountInput").min = 1; getById("selectEffectAmountInput").max = 3.99; getById("selectEffectAmountInput").step = 0.01; getById("selectEffectAmountInput3").min = 1; getById("selectEffectAmountInput3").max = 3.99; getById("selectEffectAmountInput3").step = 0.01; getById("selectEffectAmountInput").value = session.effectValue; getById("selectEffectAmountInput3").value = session.effectValue; // Show zoom position controls getById("zoomPositionControls").style.display = "block"; getById("zoomPositionControls3").style.display = "block"; } if (session.sensorData) { setupSensorData(parseInt(session.sensorData)); } if (session.externalSensorBridge) { setupExternalSensorBridge(); } if (location.protocol !== "https:") { try { //if (!session.cleanOutput) { if (["127.0.0.1", "localhost"].includes(window.location.hostname)){ // these are allowed I believe. I do change the salt however to the default one though. } else if (window.location.host.split(".")[0] !== "insecure") { console.warn("⚠️ SSL (https) is not enabled. This site will not fully work without it!

Try accessing the site from here instead.", false, false); } //} } catch (e) {} } else { try { if (navigator && navigator.mediaDevices && navigator.mediaDevices.ondevicechange) { navigator.mediaDevices.ondevicechange = reconnectDevices; } } catch (e) { errorlog(e); } } if (session.autohide && session.scene === false) { // && (session.roomid!==false)){ try { getById("main").onmouseover = showControl; // this is correct. (it's not session.showControls) document.ontouchstart = showControl; // this is correct. (it's not session.showControls) getById("gridlayout").classList.add("nocontrolbar"); if (session.autostart) { showControl(); } } catch (e) {} } if (urlParams.has("experimental")) { session.experimental = true; } if (urlParams.has("flagship")) { session.flagship = true; } //if (!session.flagship && session.mobile && (session.limitTotalBitrate===false)){ // session.limitTotalBitrate = session.totalRoomBitrate_default; // 500, with the max per guest stream out at maxMobileBitrate (350kbps) or 35-kbps if more than X in the room. //} if (urlParams.has("maxmobilebitrate")) { session.maxMobileBitrate = parseInt(urlParams.has("maxmobilebitrate")) || 0; } if (urlParams.has("lowmobilebitrate")) { session.lowMobileBitrate = parseInt(urlParams.has("lowmobilebitrate")) || 0; } // Please contact steve on discord.vdo.ninja if you'd like this iFRAME tweaked, expanded, etc -- it's updated based on user request session.remoteInterfaceAPI = function (e) { // iFRAME api support if (!e.data || typeof e.data !== "object") { warnlog(e); return; } // Ignore framegrab-audio-settings - handled by lib.js message listener if (e.data.action === "framegrab-audio-settings") { return; } log(e); try { 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") { ret = previewWebcam(); } 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 === "targetGuest") { ret = targetGuest(data.target, data.action, data.value); } else if (e.data.function === "commands" && data.action && Commands[data.action]) { ret = Commands[data.action](data.value, data.value2 || null); } else if (e.data.function === "routeMessage") { try { session.ws.onmessage({ data: e.data.value }); } catch (e) { warnlog("handshake not yet setup"); } } else if (e.data.function === "eval") { eval(e.data.value); // eval == evil ; feedback welcomed ; } } } catch (err) { errorlog(err); } if ("sendData" in e.data) { // send generic data via p2p. Send whatever you want I guess; there is a max chunk size of course. Use filetransfer for large files? var UUID = false; var streamID = false; var type = false; if (e.data.UUID) { UUID = e.data.UUID; } else if (e.data.streamID) { streamID = e.data.streamID; } if (e.data.type) { type = e.data.type; } var ret = session.sendGenericData(e.data.sendData, UUID, streamID, type); // comes out the other side as: ("dataReceived", data, UUID); if (!ret) { warnlog("Not connected yet or no peers available"); } return; } if ("PPT" in e.data) { log("PTT activated-webmain"); if (e.data.PPT === true) { // unmute session.muted = false; // set getById("mutebutton").classList.add("PPTActive"); log(session.muted); toggleMute(true); // apply } else if (e.data.PPT === false) { // mute session.muted = true; // set getById("mutebutton").classList.remove("PPTActive"); log(session.muted); toggleMute(true); // apply } else if (e.data.PPT === "toggle") { // toggle toggleMute(); } return; // this is a high-load call, so lets skip the rest of the checks to save cpu. } if ("sendChat" in e.data) { sendChat(e.data.sendChat); // sends to all peers; more options down the road return; } // Chat out gets called via getChatMessage function // Related code: parent.postMessage({"chat": {"msg":-----,"type":----,"time":---} }, "*"); // session.requestResolution(vid.dataset.UUID, wrw*window.devicePixelRatio, hrh*window.devicePixelRatio); 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 session.muted = true; // set log(session.muted); toggleMute(true); // apply } else if (e.data.mic === "toggle") { // toggle toggleMute(); } } if ("toggleSettings" 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.toggleSettings && !toggleSettingsState) { toggleSettings(); } else if (e.data.toggleSettings == "toggle") { toggleSettings(); } else if (toggleSettingsState) { toggleSettings(); } } 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 session.videoMuted = true; // set log(session.videoMuted); toggleVideoMute(true); // apply } else if (e.data.camera === "toggle") { // toggle toggleVideoMute(); } } if ("pauseinvisible" in e.data) { // whether videos hidden in the update mixer are muted or not, based on if they are visible or not. if (e.data.pauseinvisible === "toggle") { // toggle session.pauseInvisible = !session.pauseInvisible; } else if (!e.data.pauseinvisible) { session.pauseInvisible = false; } else { session.pauseInvisible = true; } updateMixer(); } if ("keyframe" in e.data) { session.sendKeyFrameScenes(); } if ("groups" in e.data) { if (typeof e.data.groups == "object") { session.group = e.data.groups || []; } else if (!e.data.group) { session.group = []; } else { session.group = e.data.groups.split(","); } var eleGroup = getById("groups"); eleGroup.querySelectorAll('[data-action-type="toggle-group"][data-group]').forEach(group => { if (!(session.group && session.group.includes(group))) { group.remove("green"); } }); if (session.group) { session.group.forEach(group => { var ele = eleGroup.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"'); if (!ele) { ele = document.createElement("div"); ele.dataset.actionType = "toggle-group"; ele.dataset.group = group; ele.classList.add("float"); ele.style.display = "inline-block"; ele.role = "button"; ele.innerHTML = '
' + group; eleGroup.appendChild(ele); ele.onclick = function () { changeGroupDirectorAPI(this.dataset.group); }; } ele.classList.add("green"); }); } updateMixer(); if (session.group.length || session.allowNoGroup) { session.sendMessage({ group: session.group.join(",") }); if (session.screenShareState && session.screenshareType === 3) { session.sendMessage({ group: session.group.join(","), altUUID: true }); } } else { session.sendMessage({ group: false }); if (session.screenShareState && session.screenshareType === 3) { session.sendMessage({ group: false, altUUID: true }); } } } if (e.data.getSnapshotBySlot || e.data.getSnapshotByStreamID) { let videoElement = false; let streamID = ("getSnapshotBySlot" in e.data) ? session.currentSlots[parseInt(e.data.getSnapshotBySlot) || 0] : e.data.getSnapshotByStreamID; let UUID = false; if (streamID){ for (let i in session.rpcs) { if (session.rpcs[i].streamID == streamID) { UUID = i; videoElement = session.rpcs[i].videoElement; break; } } } if (streamID && videoElement && videoElement.srcObject) { const videoTrack = videoElement.srcObject.getVideoTracks()[0]; if (videoTrack) { const processor = new MediaStreamTrackProcessor({ track: videoTrack }); const reader = processor.readable.getReader(); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d", { willReadFrequently: true }); try { reader.read().then(({ done, value: frame }) => { if (!done && frame) { canvas.width = frame.displayWidth; canvas.height = frame.displayHeight; ctx.drawImage(frame, 0, 0); const format = typeof session.sendframes === "string" ? session.sendframes : "png"; const imageData = canvas.toDataURL(`image/${format}`, 0.8); parent.postMessage({ type: 'frame', frame: imageData, UUID: UUID, streamID: streamID, trackID: videoTrack.id, kind: videoTrack.kind, format: format, slot: parseInt(Object.keys(session.currentSlots).find(key => session.currentSlots[key] === streamID)) || 0, cib: e.data.cib || null }, session.iframetarget); // Proper cleanup frame.close(); reader.cancel(); // Remove canvas from DOM if it was added if (canvas.parentNode) { canvas.parentNode.removeChild(canvas); } } }).catch(error => { console.error("Error processing image frame:", error); }); } catch (error) { console.error("Error setting up frame capture:", error); } } } } if ("groupView" in e.data) { if (typeof e.data.groupView == "object") { session.groupView = e.data.groupView || []; } else if (!e.data.groupView) { session.groupView = []; } else { session.groupView = e.data.groupView.split(","); } updateMixer(); } 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 ("record" in e.data) { if (e.data.record == false) { // mute if ("recording" in session.videoElement) { recordLocalVideo("stop"); } } else if (e.data.record === true) { if ("recording" in session.videoElement) { // already recording } else { recordLocalVideo("start"); } } else if (e.data.record) { var video = document.getElementById(e.data.record); if (video) { var videoKbps = 4000; if (session.recordLocal !== false) { videoKbps = session.recordLocal; } recordLocalVideo(null, videoKbps, video); } } } if ("volume" in e.data) { // might not work with iframes or meshcast currently. var requestedVolume = parseFloat(e.data.volume) || 0; if (requestedVolume > 1.0) { // this is a bit quasi improper. But the API is official 0 to 1.0; not 0 to 100, so this is mainly a catch for those not using the API right. requestedVolume = requestedVolume / 100.0; } setSessionPlaybackVolume(requestedVolume, e.data.target); } if ("enableYouTube" in e.data) { // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. if (typeof e.data.enableYouTube == "string") { session.youtubeKey = e.data.enableYouTube; } else if (!session.youtubeKey) { errorlog("No Youtube Key provided"); } console.log(session.youtubeKey); YoutubeChatInterface(true); } if ("nextSlide" in e.data) { // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. nextSlide(); } if ("prevSlide" in e.data) { // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. gobackSlide(); } if ("panning" in e.data) { // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. if ("UUID" in e.data) { try { adjustPan(UUID, e.data.panning); } catch (e) { errorlog(e); } } else { for (var i in session.rpcs) { try { adjustPan(i, e.data.panning); } catch (e) { errorlog(e); } } } } if ("targetBitrate" in e.data || "targetAudioBitrate" in e.data) { // this sets the fundemental bitrate target, but does not necessarily "lock" . var msg = {}; if ("targetBitrate" in e.data) { msg.targetBitrate = e.data.targetBitrate; } if ("targetAudioBitrate" in e.data) { msg.targetAudioBitrate = e.data.targetAudioBitrate; } if (e.data.requestAs) { msg.requestAs = e.data.requestAs; } if (e.data.remote) { msg.remote = e.data.remote; } for (var i in session.rpcs) { try { if ("streamID" in session.rpcs[i]) { if ("target" in e.data) { if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { // specify a stream ID or let it apply to all videos session.sendRequest(msg, i); } } else if (e.data.UUID && e.data.UUID === i) { session.sendRequest(msg, i); } else if (e.data.streamID) { if (session.rpcs[i].streamID == e.data.streamID) { // specify a stream ID or let it apply to all videos session.sendRequest(msg, i); } } else { session.sendRequest(msg, i); // bitrate = 0 pauses the video } } } catch (e) { errorlog(e); } } } if ("manualBitrate" in e.data) { for (var i in session.rpcs) { try { if ("streamID" in session.rpcs[i]) { if ("target" in e.data) { if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { // specify a stream ID or let it apply to all videos session.rpcs[i].manualBandwidth = e.data.manualBitrate; session.requestRateLimit(false, i); } } else if (e.data.UUID && e.data.UUID === i) { session.rpcs[i].manualBandwidth = e.data.manualBitrate; session.requestRateLimit(false, i); } else if (e.data.streamID) { if (session.rpcs[i].streamID == e.data.streamID) { // specify a stream ID or let it apply to all videos session.rpcs[i].manualBandwidth = e.data.manualBitrate; session.requestRateLimit(false, i); } } else { session.rpcs[i].manualBandwidth = e.data.manualBitrate; session.requestRateLimit(false, i); } } } catch (e) { errorlog(e); } } } if ("bitrate" in e.data) { /// set a video bitrate for a video; scene or view link; kbps var lock = true; if ("lock" in e.data) { // since this is the iframe API, we're going to assume the default is manual over-ride. VDO.Ninja's automixer logic won't override a locked bitrate. lock = e.data.lock; } for (var i in session.rpcs) { try { if ("streamID" in session.rpcs[i]) { // we only target publishers with this call if ("target" in e.data) { if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { // specify a stream ID or let it apply to all videos session.requestRateLimit(e.data.bitrate, i, false, lock); } } else if (e.data.UUID && e.data.UUID === i) { session.requestRateLimit(e.data.bitrate, i, false, lock); } else if (e.data.streamID) { if (session.rpcs[i].streamID == e.data.streamID) { // specify a stream ID or let it apply to all videos session.requestRateLimit(e.data.bitrate, i, false, lock); } } else { session.requestRateLimit(e.data.bitrate, i, false, lock); // bitrate = 0 pauses the video } } } catch (e) { errorlog(e); } } } if ("audiobitrate" in e.data) { // changes the audio bitrate of a specific or all inbound media tracks. kbps var lock = true; if ("lock" in e.data) { // since this is the iframe API, we're going to assume the default is manual over-ride. VDO.Ninja's automixer logic won't override a locked bitrate. lock = e.data.lock; } for (var i in session.rpcs) { try { if ("streamID" in session.rpcs[i]) { // we only target publishers with this call if ("target" in e.data) { if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { // specify a stream ID or let it apply to all videos session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); } } else if (e.data.UUID && e.data.UUID === i) { session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); } else if (e.data.streamID) { if (session.rpcs[i].streamID == e.data.streamID) { // specify a stream ID or let it apply to all videos session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); } } else { session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); // bitrate = 0 pauses the video } } } catch (e) { errorlog(e); } } } if ("changeVideoDevice" in e.data) { warnlog(e.data.changeVideoDevice); changeVideoDevice(e.data.changeVideoDevice); } if ("changeAudioDevice" in e.data) { warnlog(e.data.changeAudioDevice); changeAudioDevice(e.data.changeAudioDevice); } if ("changeAudioOutputDevice" in e.data) { warnlog(e.data.changeAudioOutputDevice); changeAudioOutputDeviceById(e.data.changeAudioOutputDevice); } if ("getDeviceList" in e.data) { // get a list of local camera / audio devices warnlog(e.data.getDeviceList); enumerateDevices().then(function (deviceInfos) { parent.postMessage( { deviceList: JSON.parse(JSON.stringify(deviceInfos)), cib: e.data.cib || null }, session.iframetarget ); }); } if ("sceneState" in e.data) { // TRUE OR FALSE - tells the connected peers if they are live or not via a tally light change. var msg = {}; msg.obsState = {}; msg.obsState.visibility = e.data.sceneState || false; msg.obsState.recording = e.data.sceneState || false; session.sendRequest(msg); } if ("layouts" in e.data) { session.layouts = e.data.layouts; if ("obsSceneTriggers" in e.data) { session.obsSceneTriggers = e.data.obsSceneTriggers; } else { session.obsSceneTriggers = false; } for (var uid in session.pcs) { if (session.pcs[uid].layout) { session.sendMessage(e.data, uid); } } // session.obsSceneSync(); // not sure I need to trigger this? log(e.data); } if ("sendMessage" in e.data) { // webrtc send to viewers session.sendMessage(e.data.sendMessage); } if ("sendRequest" in e.data) { // webrtc send to publishers session.sendRequest(e.data.sendRequest); } if ("sendRawMIDI" in e.data) { // webrtc send to publishers //var msg = {}; //msg.midi = {}; //msg.midi.d = e.data.sendRawMIDI.data; aka [d1,d2,d3]; //msg.midi.c = e.data.sendRawMIDI.channel; //msg.midi.s = e.data.sendRawMIDI.timestamp; // e.data.UUID or e.data.streamID or leave empty to send to all if ("UUID" in e.data) { sendRawMIDI(e.data.sendRawMIDI, e.data.UUID); // send to connection } else if (e.data.streamID) { sendRawMIDI(e.data.sendRawMIDI, false, e.data.streamID); // send to connection } else { sendRawMIDI(e.data.sendRawMIDI); // send to all } return; // make it send faster. } 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.sendPeers); } if ("reload" in e.data) { // reload the page reloadRequested(); // location.reload();, but with no user prompt (force reload) } if ("getFaces" in e.data) { if (e.data.faceTrack) { session.grabFaceData = true; getFaces(); } else { session.grabFaceData = false; } } if ("getFreshStats" in e.data) { // takes a second to query. var stats = {}; try { stats.inbound = {}; stats.total_outbound_connections = Object.keys(session.pcs).length; stats.total_inbound_connections = Object.keys(session.rpcs).length; for (var i in session.rpcs) { stats.inbound[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.id && stat.id.startsWith("DEPRECATED_")) { return; } 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 ("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.candidateType_remote = 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.candidateType_local = stat.candidateType; } } else if (stat.type == "candidate-pair" && stat.nominated) { if ("availableOutgoingBitrate" in stat) { session.pcs[UUID].stats.available_outgoing_bitrate_kbps = parseInt(stat.availableOutgoingBitrate / 1024); } if ("totalRoundTripTime" in stat) { if ("responsesReceived" in stat) { session.pcs[UUID].stats.average_roundTripTime_ms = parseInt((stat.totalRoundTripTime / stat.responsesReceived) * 1000); } } } return; }); return; }); }, 0, uuid ); } } catch (e) { // disconnected probably } setTimeout(function () { stats.outbound_stats = {}; try { for (var i in session.pcs) { stats.outbound_stats[i] = session.pcs[i].stats; } } catch (e) {} parent.postMessage( { stats: stats, cib: e.data.cib || null }, session.iframetarget ); }, 1000); } if ("getStats" in e.data) { if (e.data.streamID) { parent.postMessage( { stats: getQuickStats(e.data.streamID), cib: e.data.cib || null }, session.iframetarget ); } else { parent.postMessage( { stats: getQuickStats(), cib: e.data.cib || null }, session.iframetarget ); } } if ("getRemoteStats" in e.data) { if (session.remote) { session.sendRequest({ requestStats: true, remote: session.remote }); } else { session.sendRequest({ requestStats: true }); } } if ("requestStatsContinuous" in e.data) { if (session.remote) { session.sendRequest({ requestStatsContinuous: e.data.requestStatsContinuous, remote: session.remote }); } else { session.sendRequest({ requestStatsContinuous: e.data.requestStatsContinuous }); } } if ("getLoudness" in e.data) { log("GOT LOUDNESS REQUEST"); if (e.data.getLoudness == true) { if (!session.pushLoudness && session.audioEffects !== true) { session.pushLoudness = true; for (var i in session.rpcs) { updateIncomingAudioElement(i); // this can be called when turning on/off inbound audio processing. } } else { session.pushLoudness = true; } var loudness = {}; for (var i in session.rpcs) { loudness[session.rpcs[i].streamID] = session.rpcs[i].stats.Audio_Loudness; } parent.postMessage( { loudness: loudness, cib: e.data.cib || null }, session.iframetarget ); } else { if (session.pushLoudness && !session.audioEffects) { // turn off audio processing session.pushLoudness = false; for (var i in session.rpcs) { updateIncomingAudioElement(i); } } else { session.pushLoudness = false; // can't turn off audio processing } } } if ("getEffectsData" in e.data) { log("GOT getEffects Data REQUESTed"); // face tracking info, etc. if (e.data.getEffectsData !== false) { session.pushEffectsData = e.data.getEffectsData; // which effect do you want the data from? it won't enable the effect necessarily; just the ML pipeline //parent.postMessage({ // "effectsData": effectsData, // "effectsID": session.pushEffectsData //}, session.iframetarget); } else { session.pushEffectsData = false; } } if ("getStreamIDs" in e.data) { // get a list of stream Ids, with a label if it is present. label = false if not there if (e.data.getStreamIDs) { var streamIDs = {}; for (var i in session.rpcs) { streamIDs[session.rpcs[i].streamID] = session.rpcs[i].label; } parent.postMessage( { streamIDs: streamIDs, cib: e.data.cib || null }, session.iframetarget ); } } if ("getStreamInfo" in e.data) { // get a list of stream Ids, with a label if it is present. label = false if not there try { var UUIDS = {}; for (var i in session.rpcs) { UUIDS[i] = {}; UUIDS[i].label = session.rpcs[i].label || false; UUIDS[i].streamID = session.rpcs[i].streamID || false; if (session.rpcs[i].stats && session.rpcs[i].stats.info) { UUIDS[i].info = session.rpcs[i].stats.info; } else { UUIDS[i].info = {}; } } parent.postMessage( { streamInfo: UUIDS, cib: e.data.cib || null }, session.iframetarget ); } catch (e) { errorlog(e); } } if ("close" in e.data || "hangup" in e.data) { // disconnect and hangup all inbound streams. var tmp = e.data.close || e.data.hangup; if (tmp == "estop") { // try to stop the video recording even if not complete; if you can't wait even ms before a reload/exit. console.log("ESTOP"); session.hangup(false, true); } else if (tmp == "reload") { // stop and reload the page safely. session.hangup(true); } else { // just hangup, but can take up to 1-second to do so fully. session.hangup(); } } //if ("hangup" in e.data) { // disconnect and hangup all inbound streams. // session.hangup(); //} if ("style" in e.data) { // insert a custom style sheet try { const style = document.createElement("style"); style.textContent = e.data.style; document.head.append(style); log(style); } catch (e) { errorlog(e); } } if ("getDetailedState" in e.data) { var detailedState = getDetailedState(); parent.postMessage( { detailedState: detailedState, cib: e.data.cib || null }, session.iframetarget ); } if ("getGuestList" in e.data) { var guestList = getGuestList(); parent.postMessage( { guestList: guestList, cib: e.data.cib || null }, session.iframetarget ); } // saveVideoFrameToDisk(video); if ("saveVideoFrameToDisk" in e.data) { let filename = false; if ("filename" in e.data) { filename = e.data.filename; if (filename.split(".").length == 1) { filename += ".png"; } } if ("streamID" in e.data) { let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); if (session.rpcs[UUID]) { if (session.rpcs[UUID].videoElement) { saveVideoFrameToDisk(session.rpcs[UUID].videoElement, false, filename); } else { errorlog("The specified video does not exist"); } } else { errorlog("The stream ID specified does not exist"); } } else if ("UUID" in e.data) { if (e.data.UUID === "*") { for (var uuid in session.rpcs) { if (session.rpcs[uuid].videoElement) { saveVideoFrameToDisk(session.rpcs[uuid].videoElement, false, filename); } } } else if (session.rpcs[e.data.UUID]) { if (session.rpcs[e.data.UUID].videoElement) { saveVideoFrameToDisk(session.rpcs[e.data.UUID].videoElement, false, filename); } else { errorlog("The specified video does not exist"); } } else { errorlog("The UUID specified does not exist"); } } else if (session.videoElement) { saveVideoFrameToDisk(session.videoElement, false, filename); } } if ("getVideoFrame" in e.data) { if ("streamID" in e.data) { let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); if (session.rpcs[UUID]) { if (session.rpcs[UUID].videoElement) { sendVideoFrameToIframe(session.rpcs[UUID].videoElement, false, e.data); } else { errorlog("The specified video does not exist"); } } else { errorlog("The stream ID specified does not exist"); } } else if ("UUID" in e.data) { if (session.rpcs[e.data.UUID]) { if (session.rpcs[e.data.UUID].videoElement) { sendVideoFrameToIframe(session.rpcs[e.data.UUID].videoElement, false, e.data); } else { errorlog("The specified video does not exist"); } } else { errorlog("The UUID specified does not exist"); } } else if (session.videoElement) { sendVideoFrameToIframe(session.videoElement, false, e.data); } } if ("copyVideoFrameToClipboard" in e.data) { if ("streamID" in e.data) { let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); if (session.rpcs[UUID]) { if (session.rpcs[UUID].videoElement) { copyVideoFrameToClipboard(session.rpcs[UUID].videoElement, false); } else { errorlog("The specified video does not exist"); } } else { errorlog("The stream ID specified does not exist"); } } else if ("UUID" in e.data) { if (session.rpcs[e.data.UUID]) { if (session.rpcs[e.data.UUID].videoElement) { copyVideoFrameToClipboard(session.rpcs[e.data.UUID].videoElement, false); } else { errorlog("The specified video does not exist"); } } else { errorlog("The UUID specified does not exist"); } } else if (session.videoElement) { copyVideoFrameToClipboard(session.videoElement, false); } } if ("setBufferDelay" in e.data) { // milliseconds console.log(e.data); let delay = parseInt(e.data.setBufferDelay) || 0; if ("streamID" in e.data) { let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); if (session.rpcs[UUID]) { session.rpcs[UUID].buffer = delay; playoutdelay(UUID); } else { errorlog("The stream ID specified does not exist"); } document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + UUID + '"] input[data-buffer-value]').forEach(ele => { ele.value = delay; }); } else if ("label" in e.data) { for (let uuid in session.rpcs) { if (session.rpcs[uuid].label && session.rpcs[uuid].label === e.data.label) { if (session.rpcs[uuid]) { session.rpcs[uuid].buffer = delay; playoutdelay(uuid); } else { errorlog("The label specified does not exist"); } } } document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + UUID + '"] input[data-buffer-value]').forEach(ele => { ele.value = delay; }); } else if ("UUID" in e.data) { if (e.data.UUID === "*") { for (var uuid in session.rpcs) { session.rpcs[uuid].buffer = delay; playoutdelay(uuid); document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + uuid + '"] input[data-buffer-value]').forEach(ele => { ele.value = delay; }); } } else if (session.rpcs[e.data.UUID]) { session.rpcs[e.data.UUID].buffer = delay; playoutdelay(e.data.UUID); document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + e.data.UUID + '"] input[data-buffer-value]').forEach(ele => { ele.value = delay; }); } else { errorlog("The UUID specified does not exist"); } } else { session.buffer = delay; // set the default buffer delay only } } if ("automixer" in e.data) { // stop the auto mixer if you want to control the layout and bitrate yourself if (e.data.automixer == true) { session.manual = session.manual === null ? false : session.manual; try { updateMixer(); } catch (e) {} } else if (e.data.automixer == false) { session.manual = session.manual === null ? true : session.manual; } } if ("advancedMode" in e.data) { if (e.data.advancedMode) { // Un-hiding advanced items document.querySelectorAll(".advanced").forEach(element => { element.classList.remove("hide"); }); } else { // Hiding advanced items document.querySelectorAll(".advanced").forEach(element => { element.classList.add("hide"); }); } } if ("requestStream" in e.data) { if (e.data.requestStream) { // load a specific stream ID log("requestStream iframe api"); session.requestStream(e.data.requestStream); } // don't use if the stream is in your room (as not needed) } // you can load a stream ID from inside a room that exists outside any room if ("layout" in e.data) { if (Array.isArray(e.data.layout)){ session.layout_array = e.data.layout; session.layout = combinedLayout(session.layout_array); } else { session.layout_array = null; session.layout = e.data.layout; } // update mixer is run later, some lines down. pokeIframeAPI("layout-updated", session.layout, null, null, e.data.cib); } if ("previewMode" in e.data) { switchModes(e.data.previewMode); } else if ("layout" in e.data) { warnlog("changing layout request via IFRAME API"); if (e.data.obsCommand) { issueLayoutOBS(e.data); } else if (session.director) { // only a director can issue a layout if ("scene" in e.data) { if ("UUID" in e.data) { issueLayout(e.data.scene, e.data.UUID); } else { issueLayout(e.data.scene); } } else if ("UUID" in e.data) { issueLayout(false, e.data.UUID); } } updateMixer(); } else if (e.data.obsCommand) { var msg = {}; msg.obsCommand = e.data.obsCommand; if (e.data.remote) { msg.remote = e.data.remote; } else { msg.remote = session.remote; } if (e.data.UUID) { msg.UUID = e.data.UUID; } if (e.data.streamID) { msg.streamID = e.data.streamID; } session.encodeRemote(msg).then(msgx => { session.sendMessage(msgx); // just to be safe; avoids spamming of wss log(msgx); }); } if ("slotmode" in e.data) { if (session.slotmode) { session.slotmode = parseInt(e.data.slotmode); populateSlotPicker(); } else { session.slotmode = false; } } //////////// manual scale. Request a specific down-scaled resolution from a remote connection var targetWidth = false; var targetHeight = false; if ("targetWidth" in e.data) { targetWidth = e.data.targetWidth || 0; } if ("targetHeight" in e.data) { targetHeight = e.data.targetHeight || 0; } // session.viewheight or session.viewwidth if ((targetWidth || targetHeight) && e.data.UUID) { var requestAs = false; if (e.data.requestAs) { requestAs = e.data.requestAs; } session.requestResolution(e.data.UUID, targetWidth || 4096, targetHeight || 2160, false, requestAs); // this is fine. } //////////////// if ("scale" in e.data) { if (e.data.scale === false) { session.dynamicScale = true; // disable manual scaling updateMixer(); var scale = false; } else { session.dynamicScale = false; var scale = parseInt(e.data.scale) || 100; } if (e.data.UUID) { session.sendRequest({ scale: scale }, UUID); } else if (e.data.target) { for (var i in session.rpcs) { try { if ("streamID" in session.rpcs[i]) { if ("target" in e.data) { if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { // specify a stream ID or let it apply to all videos session.sendRequest({ scale: scale }, i); } } else { session.sendRequest({ scale: scale }, i); } } } catch (e) { errorlog(e); } } } else { session.sendRequest({ scale: scale }); } } if ("action" in e.data && e.data.action != "null") { /////////////// reuse the Companion API var resp = processMessage(e.data); // reuse the companion API if (resp !== null) { log(resp); parent.postMessage(resp, session.iframetarget, null, null, e.data.cib); } } else if ("target" in e.data) { log(e.data); 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) { try { for (const property in e.data.settings) { try { session.rpcs[i].videoElement[property] = e.data.settings[property]; } catch (e) {} } } catch (e) {} } if ("add" in e.data) { try { getById("gridlayout").appendChild(session.rpcs[i].videoElement); } catch (e) { warnlog(e); } } else if ("remove" in e.data) { try { session.rpcs[i].videoElement.parentNode.removeChild(session.rpcs[i].videoElement); } catch (e) { try { session.rpcs[i].videoElement.parentNode.parentNode.removeChild(session.rpcs[i].videoElement.parentNode); } catch (e) {} } } else if ("replace" in e.data) { // should allow for a cleaner cut between two video streams. try { getById("gridlayout").appendChild(session.rpcs[i].videoElement); getById("gridlayout").childNodes.forEach(ele => { if (!ele.id || ele.id !== session.rpcs[i].videoElement.id) { getById("gridlayout").removeChild(ele); } }); } catch (e) {} } } catch (e) { errorlog(e); } } } } catch (e) { errorlog(e); } } } }; if (isIFrame) { // reduce CPU load if not needed. //iframe API window.onmessage = session.remoteInterfaceAPI; } if (session.midiHotkeys || session.midiOut !== false) { var script = document.createElement("script"); script.onload = function () { WebMidi.enable({ sysex: true }) .then(() => { WebMidi.timeStart = Date.now(); // start time WebMidi.addListener("connected", function (e) { log(e); }); WebMidi.addListener("disconnected", function (e) { log(e); }); console.log(WebMidi.inputs); if (session.midiOut === true) { for (var i = 0; i < WebMidi.inputs.length; i++) { try { var input = WebMidi.inputs[i]; input.addListener("midimessage", function (e) { //log(e); e.timestamp += WebMidi.timeStart; sendRawMIDI(e); //var msg = {}; //msg.midi = {}; //msg.midi.d = e.data; aka [d1,d2,d3]; //msg.midi.c = e.channel; //msg.midi.s = e.timestamp; }); } catch (e) {} } } else if (session.midiOut == parseInt(session.midiOut)) { try { var input = WebMidi.inputs[parseInt(session.midiOut) - 1]; input.addListener("midimessage", function (e) { e.timestamp += WebMidi.timeStart; sendRawMIDI(e); }); } catch (e) { errorlog(e); } } for (var i = 0; i < WebMidi.inputs.length; i++) { if (session.midiDevice && session.midiDevice !== i + 1) { continue; } var input = WebMidi.inputs[i]; if (session.midiChannel) { input = input.channels[session.midiChannel]; } if (session.midiHotkeys == 4) { input.addListener("controlchange", function (e) { log(e); midiHotkeysCommand(e.controller.number, e.rawValue); }); } else if (session.midiHotkeys == 5) { if (session.midiOffset !== false) { input.addListener("controlchange", function (e) { midiHotkeysCommand_offset(e.controller.number, e.rawValue, session.midiOffset); }); } } else { input.addListener("noteon", function (e) { log(e); var note = e.note.name + e.note.octave; var velocity = e.velocity || false; midiHotkeysNote(note, velocity); }); } } }) .catch(errorlog); }; script.src = "./thirdparty/webmidi3.js"; // dynamically load this only if its needed. Keeps loading time down. document.head.appendChild(script); } else if (session.midiIn) { var script = document.createElement("script"); script.src = "./thirdparty/webmidi3.js"; // dynamically load this only if its needed. Keeps loading time down. script.onload = function () { WebMidi.enable({ sysex: true }) .then(() => console.log(WebMidi.outputs)) .catch(errorlog); }; document.head.appendChild(script); } var languages = getById("languagesList").querySelectorAll("li a"); var timezones = []; languages.forEach(language => { if (language.dataset.tz) { var languageTimezones = language.dataset.tz.split(";"); // each link can have multiple timezones separated by ; languageTimezones.forEach(element => { timezones.push(element); }); } }); var currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; if (timezones.includes(currentTimezone)) { var list = getById("languagesList"); // Find the link element matching the timezone var el = list.querySelector("li a[data-tz*='" + currentTimezone + "']"); if (el) { var targetLi = el.parentElement; // Get the parent
  • var firstLi = list.querySelector("li:first-child"); // Get the first li (English) // Only move if it's not already the first or second element if (targetLi && firstLi && targetLi !== firstLi && targetLi !== firstLi.nextSibling) { // Insert the
  • after the first element (English) list.insertBefore(targetLi, firstLi.nextSibling); // The original
  • is automatically removed from its old position when inserted elsewhere. } } } var visAudioTimeout = null; document.addEventListener("visibilitychange", function () { //log("hidden : " +document.hidden); //log("vis : "+document.visibilityState); if (iOS || iPad) { // fixes a bug on iOS devices. Not need with other devices? toggleAutoVideoMute(); clearTimeout(visAudioTimeout); if (document.visibilityState === "visible") { visAudioTimeout = setTimeout(function () { resetupAudioOut(); activatedPreview = false; grabAudio("#audioSource3"); }, 500); } } }); // Warns user about network going down window.addEventListener("offline", function (e) { warnlog("connection lost"); if (((session.view!==false) || session.whepInput || session.whipView) && session.permaid === false) { log("VDO.Ninja has no network connectivity and can't work properly."); } else if (session.scene !== false) { log("VDO.Ninja has no network connectivity and can't work properly."); } else if (!session.cleanOutput) { if (iOS || iPad) { for (var UUID in session.pcs) { session.pcs[UUID].close(); delete session.pcs[UUID]; session.applySoloChat(); applySceneState(); } } if (location.hostname === "vdo.ninja") { warnUser(getTranslation("no-network-details")); } else { warnUser(getTranslation("no-network")); } } else { log("VDO.Ninja has no network connectivity and can't work properly."); } }); window.addEventListener("online", function (e) { log("Back ONLINE"); closeModal(); if (!session.onceConnected) { // never connected to websockets before. Let's not trigger retryWatchInterval if we don't have to. return; } if (!session.retryWatchInterval()) { // ask for the streams again to watch session.ping(); // if no streams requested, let's ping instead. } }); /* function updateConnectionStatus() { // no longer works in chrome. try{ if (!session.stats){ return; } if (Connection.type){ log("Connection type changed from " + session.stats.network_type + " to " + Connection.type); if (session.stats.network_type && (session.stats.network_type !== Connection.type)){ var miniInfo = {}; miniInfo.con = Connection.type; session.sendMessage({"miniInfo":miniInfo}); if (!session.retryWatchInterval()){ // ask for the streams again to watch session.ping(); // if no streams requested, let's ping instead. } } else { // connection state changed, but doesn't seem like it actually changed... session.ping(); // if no streams requested, let's ping instead. } session.stats.network_type = Connection.type; } } catch(e){warnlog(e);} } try { var Connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (Connection){ if (Connection.type){ session.stats.network_type = Connection.type } Connection.addEventListener('change', updateConnectionStatus); } } catch (e) {log(e);} // effectiveType is not yet supported by Firefox or Safari; 2021 */ setInterval(function () { checkConnection(); }, 5000); // Remove modal if network comes back up window.addEventListener("online", function (e) { if (!session.cleanOutput) { // Remove last inserted modal; Could be improved by tagging the // modal elements and only removing modals tagged 'offline' let userWarnings = document.querySelectorAll(".alertModal"); closeModal(userWarnings[userWarnings.length - 1]); } else { log("Network connectivity has been restored."); } }); document.addEventListener("DOMContentLoaded", function () { var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy")); if ("IntersectionObserver" in window) { var lazyVideoObserver = new IntersectionObserver(function (entries, observer) { entries.forEach(function (video) { if (video.isIntersecting) { for (var source in video.target.children) { var videoSource = video.target.children[source]; if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") { videoSource.src = videoSource.dataset.src; } } video.target.load(); video.target.classList.remove("lazy"); lazyVideoObserver.unobserve(video.target); } }); }); lazyVideos.forEach(function (lazyVideo) { lazyVideoObserver.observe(lazyVideo); }); } armWakeLockOnInteraction(); acquireWakeLock(); // Re-acquire wake lock when the page becomes visible again, as that's a requirement for wakelock document.addEventListener('visibilitychange', handleVisibilityChangeWakeLock); // Initialize fullscreen/PIP button settings from localStorage initButtonToggleSettings(); }); 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=VDO.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(getById("dragImage"), 24, 24); event.dataTransfer.setData("text/uri-list", encodeURI(url)); }); if (navigator.getBattery) { navigator.getBattery().then(function (battery) { session.batteryState = {}; if ("level" in battery) { session.batteryState.level = battery.level; } if ("charging" in battery) { session.batteryState.charging = battery.charging; } if (session.batteryState == {}) { session.batteryState = null; } battery.addEventListener("chargingchange", function () { session.batteryState = {}; var miniInfo = {}; if ("level" in battery) { session.batteryState.level = battery.level; miniInfo.bat = battery.level; } if ("charging" in battery) { session.batteryState.charging = battery.charging; miniInfo.chrg = battery.charging; } if (session.batteryState == {}) { session.batteryState = null; } session.sendMessage({ miniInfo: miniInfo }); }); battery.addEventListener("levelchange", function () { session.batteryState = {}; var miniInfo = {}; console.log(session.batteryState); if ("charging" in battery) { session.batteryState.charging = battery.charging; miniInfo.chrg = battery.charging; } if ("level" in battery) { session.batteryState.level = battery.level; miniInfo.bat = battery.level; if (!session.batteryState.charging && battery.level == 0.02) { warnlog("Very Low Battery - triggering auto saves"); try { if (session.screenShareElement && session.screenShareElement.recorder && session.screenShareElement.recorder.setupWriter) { session.screenShareElement.recorder.setupWriter(session.screenShareElement); } if (session.videoElement && session.videoElement.recorder && session.videoElement.recorder.setupWriter) { session.videoElement.recorder.setupWriter(session.videoElement); } } catch (e) { errorlog(e); } } } if (session.batteryState == {}) { session.batteryState = null; } session.sendMessage({ miniInfo: miniInfo }); }); }); } var lastTouchEnd = 0; document.addEventListener( "touchend", function (event) { var now = new Date().getTime(); if (now - lastTouchEnd <= 300) { event.preventDefault(); } lastTouchEnd = now; }, false ); document.addEventListener("click", function (event) { if (session.firstPlayTriggered == false) { playAllVideos(); session.firstPlayTriggered = true; try { if (session.audioCtx && session.audioCtx.state == "suspended") { session.audioCtx.resume(); } } catch (e) { warnlog("session.audioCtx.resume(); failed 4"); } history.pushState({}, ""); } }); document.addEventListener("keydown", event => { keyDownEvent(event); }); function keyDownEvent(event) { if (event.ctrlKey || event.metaKey) { // detect if CTRL is pressed CtrlPressed = true; } else { CtrlPressed = false; } if (event.altKey) { AltPressed = true; } else { AltPressed = false; } if (event.key === "Escape") { log("escape pressed; checking to see if modal box opened and will close"); if (document.fullscreenElement) { document.exitFullscreen(); //updateMixer(); } else { let userWarnings = document.querySelectorAll(".alertModal, .promptModal"); if (userWarnings.length) { closeModal(userWarnings[userWarnings.length - 1]); } } return; } if (session.disableHotKeys) { return; } if (PPTHotkey) { if (event.target && event.target.tagName == "INPUT") { // skip, since an input field is selected } else if (PPTHotkey.ctrl === event.ctrlKey && PPTHotkey.alt === AltPressed && PPTHotkey.meta === event.metaKey && (PPTHotkey.key === false || (PPTHotkey.key !== false && PPTHotkey.key === event.key))) { if (session.muted && !PPTKeyPressed) { session.muted = false; PPTKeyPressed = true; getById("mutebutton").classList.add("PPTActive"); toggleMute(true); } else if (!PPTKeyPressed) { PPTKeyPressed = true; getById("mutebutton").classList.add("PPTActive"); } event.preventDefault(); event.stopPropagation(); return; } else if (PPTKeyPressed) { PPTKeyPressed = false; getById("mutebutton").classList.remove("PPTActive"); if (!session.muted) { session.muted = true; toggleMute(true); } event.preventDefault(); event.stopPropagation(); return; } } if (KeyPressedTimeout || PPTKeyPressed) { event.preventDefault(); event.stopPropagation(); return; } if (CtrlPressed && event.keyCode) { if (event.keyCode == 77) { // M if (event.metaKey) { if (AltPressed) { if (!KeyPressedTimeout) { toggleMute(); // macOS KeyPressedTimeout = Date.now(); event.preventDefault(); event.stopPropagation(); return; } } } else { if (!KeyPressedTimeout) { toggleMute(); // Windows KeyPressedTimeout = Date.now(); event.preventDefault(); event.stopPropagation(); return; } } } else if (event.keyCode == 66) { // B toggleVideoMute(); event.preventDefault(); event.stopPropagation(); return; } if (AltPressed) { // CTRL + ALT if (event.keyCode == 70) { // F toggleFileshare(); event.preventDefault(); event.stopPropagation(); return; } else if (event.keyCode == 67) { // C cycleCameras(); event.preventDefault(); event.stopPropagation(); return; } else if (event.keyCode == 83) { // S toggleScreenShare(); event.preventDefault(); event.stopPropagation(); return; } else if (event.keyCode == 68) { // D if (!drawOnScreenObject) { drawOnScreen(); } else { drawOnScreenObject.stop(); } event.preventDefault(); event.stopPropagation(); return; } else if (event.keyCode == 80) { // P if (session.videoElement) { togglePictureInPicture(session.videoElement); event.preventDefault(); event.stopPropagation(); return; } } } } else if (AltPressed && event.keyCode) { if (event.keyCode == 65) { // A toggleSpeakerMute(); event.preventDefault(); event.stopPropagation(); return; } else if (event.key === "s") { if (document.getElementById("gowebcam") && document.getElementById("gowebcam").dataset.ready == "true") { publishWebcam(document.getElementById("gowebcam")); } } } } document.addEventListener("mouseup", event => { MousePressed = false; }); document.addEventListener("mousedown", event => { MousePressed = true; }); document.addEventListener("keyup", event => { if (PPTKeyPressed) { PPTKeyPressed = false; getById("mutebutton").classList.remove("PPTActive"); if (!session.muted) { session.muted = true; toggleMute(true); } event.preventDefault(); event.stopPropagation(); return; } if (!(event.ctrlKey || event.metaKey)) { if (CtrlPressed) { CtrlPressed = false; for (var i in Callbacks) { var cb = Callbacks[i]; cb[0](...cb.slice(1)); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#A_better_apply } Callbacks = []; } } if (!event.altKey) { AltPressed = false; } if (event.altKey && event.shiftKey && event.keyCode === 67 /* C */) { toggleControlBar(); } if (KeyPressedTimeout && (event.keyCode == 77 || !(event.ctrlKey || event.metaKey))) { if (Date.now() - KeyPressedTimeout > 300) { toggleMute(); } if (event.keyCode == 77) { KeyPressedTimeout = 0; } } }); try { navigator.serviceWorker .getRegistrations() .then(registrations => { // getting rid of old service workers. try { log(registrations); for (let registration of registrations) { if (registration.scope) { if (registration.scope.includes("/notifications/")) {continue;} registration.unregister(); console.warn("unregistering: " + registration.scope); } } } catch (e) {} }) .catch(errorlog); } catch (e) {} setTimeout(function () { // lets lazy load the following.. window.addEventListener("beforeunload", confirmUnload); // This just keeps people from killing the live stream accidentally. Also give me a headsup that the stream is ending window.addEventListener("unload", function (e) { try { if (session.ws) { session.ws.close(); } if (session.videoElement.recording) { session.videoElement.recorder.writer.close(); session.videoElement.recording = false; } for (var 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.hangup(); } catch (e) { errorlog(e); } }); var script = document.createElement("script"); document.head.appendChild(script); script.onload = function () { var script = document.createElement("script"); document.head.appendChild(script); if (SafariVersion && SafariVersion <= 15) { // blob mode not needed since iOS 15.6 script.src = "./thirdparty/StreamSaver_legacy.js?v=2"; // blob mode for Safari } else { script.src = "./thirdparty/StreamSaver.js?v=29"; // do not use blob mode } }; script.src = "./thirdparty/polyfill.min.js"; // dynamically load this only if its needed. Keeps loading time down. }, 100); }