/*
 *  Copyright (c) 2024 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 */

///// For the debug output, uncomment this section.
/*
let lastLogTime = performance.now(); // Initialize with the current time
function getTimeStamp() {
  const now = performance.now();
  const timeSinceLastLog = now - lastLogTime;
  lastLogTime = now; // Update lastLogTime to the current time
  return timeSinceLastLog.toFixed(0); // Return time with three decimals for milliseconds
}
function getStackTrace() {
  const obj = {};
  Error.captureStackTrace(obj, getStackTrace);
  return obj.stack;
}
function getLineNumber() {
  const e = new Error();
  const frame = e.stack.split("\n")[3]; // Change the index if needed
  const lineNumber = frame.split(":").reverse()[1];
  return lineNumber;
}
function log(msg) {
  const timeStamp = getTimeStamp();
  const lineNumber = getLineNumber();
  console.log(`${timeStamp}ms [Line ${lineNumber}]:`, msg);
}
function warnlog(msg) {
  const timeStamp = getTimeStamp();
  const lineNumber = getLineNumber();
  console.warn(`${timeStamp}ms [Line ${lineNumber}]:`, msg);
}
function errorlog(msg) {
  const timeStamp = getTimeStamp();
  const lineNumber = getLineNumber();
  console.error(`${timeStamp}ms [Line ${lineNumber}]:`, msg);
}
*/
///////

var formSubmitting = true;
var activatedPreview = false;
var screensharesupport = true;
var FirefoxEnumerated = false;

var Callbacks = [];
var CtrlPressed = false; // global
var MousePressed = false; // global
var AltPressed = false;
var KeyPressedTimeout = 0;
var PPTKeyPressed = false;

var translation = false;

var miscTranslations = {
	// i can replace this list from time to time from the generated one in blank.json using translate.js
	start: "START",
	"new-display-name": "Enter a new Display Name for this stream",
	"submit-error-report": "Press OK to submit any error logs to VDO.Ninja. Error logs may contain private information.",
	"director-redirect-1": "The director wishes to redirect you to the URL: ",
	"director-redirect-2": "\n\nPress OK to be redirected.",
	"add-a-label": "Add a label",
	"audio-processing-disabled": "Audio processing is disabled with this guest. Can't mute or change volume",
	"not-the-director": "<span color='red'>You are not the director of this room. You will have limited to no control. See <a target='_blank' href='https://docs.vdo.ninja/director-settings/codirector'>&codirector</a> on how to become a co-director.</span>",
	"room-is-claimed": "The room is already claimed by someone else.\n\nOnly the first person to join a room is the assigned director.\n\nRefresh after the first director leaves to claim.",
	"token-room-is-claimed": "The room is claimed by someone else.\n\nJoin as a guest or co-director instead.",
	"room-is-claimed-codirector": "The room is already claimed by someone else.\n\nTrying to join as a co-director...",
	"streamid-already-published": "The stream ID you are publishing to is already in use.\n\nPlease try with a different invite link or refresh to retry again.\n\nYou will now be disconnected.",
	"streamid-already-published-obvious": "The stream ID you are publishing to is already in use.\n\nPlease consider using a password or a more varied/unique stream ID to avoid this issue.\n\nYou will now be disconnected.",
	"director": "Director",
	"unknown-user": "Unknown User",
	"room-test-not-good": "The room name 'test' is very commonly used and may not be secure.\n\nAre you sure you wish to proceed?",
	"load-previous-session": "Would you like to load your previous session's settings?",
	"enter-password": "Please enter the password below: \n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)",
	"enter-password-2": "Please enter the password below: \n\n(Note: Passwords are case-sensitive.)",
	"enter-director-password": "Please enter the director's password:\n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)",
	"password-incorrect": "The password was incorrect.\n\nRefresh and try again.",
	"enter-display-name": "Please enter your display name:",
	"enter-new-display-name": "Enter a new Display Name for this stream",
	"what-bitrate": "This remote guest will save the recording directly to their local disk.\n\n - The recording can fail, so have backup recordings going!\n\n - This record option does not use Internet bandwidth and offers a high quality recording\n\n - Guests using iPhones, Androids, or Safari will often have issues - bewarned.",
	"what-bitrate-gdrive": "This remote guest will save the recording directly to their local disk, as well as send a copy to your Google Drive (recordings folder).\n\n - The recording can fail however, so have backup recordings going!\n\n - Guests using iPhones, Androids, or Safari will often have issues - bewarned.",
	"enter-website": "Enter a website URL to share",
	"press-ok-to-record": " - Keep this browser tab active when recording.\n\n - This recording option will record to your local download folder.\n\n - Quality may depend on the Internet connection between you and the guest.\n\n - Recordings may possibly fail; have a backup option!",
	"no-streamID-provided": "No streamID was provided; one will be generated randomily.\n\nStream ID: ",
	"alphanumeric-only": "Info: Only AlphaNumeric characters should be used for the stream ID.\n\nThe offending characters have been replaced by an underscore",
	"stream-id-too-long": "The Stream ID should be less than 64 alPhaNuMeric characters long.\n\nWe will trim it to length.",
	"share-with-trusted": "Share only with those you trust",
	"pass-recommended": "A password is recommended",
	"insecure-room-name": "Insecure room name.",
	"allowed-chars": "Allowed chars",
	"transfer": "transfer",
	"armed": "armed",
	"transfer-guest-to-room": "Transfer guests to room:\n\n(Please note: rooms must share the same password)",
	"transfer-guest-to-url": "Transfer guests to new website URL.\n\nGuests will be prompted to accept unless they are using &consent",
	"change-url": "change URL",
	"mute-in-scene": "mute in scene",
	"unmute-guest": "unmute guest",
	"unmute": "unmute",
	"undeafen": "undeafen",
	"deafen": "deafen guest",
	"unblind": "unblind",
	"blind": "blind guest",
	"mute-guest": "mute guest",
	"mute": "mute",
	"unhide": "unhide guest",
	"hide-guest": "Hide",
	"insecure-stream-id": "⚠️ Insecure stream ID detected\n\nIt is strongly advised that a password is used if using a short non-unique stream ID.\n\nThis is just a warning that can be ignored.",
	"confirm-disconnect-users": "Are you sure you wish to disconnect these users?",
	"confirm-disconnect-user": "Are you sure you wish to disconnect this user?",
	"enter-new-codirector-password": "Enter a co-director password to use",
	"control-room-co-director": "Control Room: Co-Director",
	"volume-control": "Volume control for local playback only",
	"signal-meter": "Video packet loss indicator of video preview; green is good, red is bad. Flame implies CPU is overloaded. May not reflect the packet loss seen by scenes or other guests.",
	"waiting-for-the-stream": "Waiting for the stream. Tip: Adding &cleanoutput to the URL will hide this spinner, or click to retry, which will also hide it.",
	"main-director": "Main Director",
	"co-director": "Co-Director",
	"share-a-screen": "Share a screen",
	"stop-screen-sharing": "Stop screen sharing",
	"you-have-been-transferred": "You've been transferred to a different room",
	"you-have-been-activated": "The director has now allowed you to see others in the room",
	"you-not-yet-activated": "Please wait until the director brings you into the room",
	"you-are-no-longer-a-co-director": "You are no longer a co-director as you were transferred.",
	"transferred": "Transferred",
	"room-changed": "Your room has changed",
	"headphones-tip": "<i>Tip:</i> Use headphones to avoid audio echo issues.",
	"camera-tip-c922": "<i>Tip:</i> To achieve 60-fps with a C922 webcam, low-light compensation needs to be turned off, exposure set to auto, and 720p used.",
	"camera-tip-camlink": "<i>Tip:</i> A Cam Link may glitch green/purple if accessed elsewhere while already in use.",
	"samsung-a-series": "Samsung A-series phones may have issues with Chrome; if so, try Firefox Mobile instead or switch video codecs.",
	"screen-permissions-denied": "Permission to capture denied. Ensure your browser has screen record system permissions\n\n1.On your Mac, choose Apple menu  > System Preferences, click Security & Privacy , then click Privacy.\n2.Select Screen Recording.\n3.Select the checkbox next to your browser to allow it to record your screen.",
	"change-audio-output-device": "Audio could not be captured.\n\nIf you need audio, please make sure you have an audio output device available.\n\nSome gaming headsets (ie: Logitech/Corsair) also may need to be set to 2-channel output to work, as <a target='_blank' href='https://docs.vdo.ninja/common-errors-and-known-issues/surround-sound-error-when-screen-sharing-with-usb-headset'>surround sound drivers may cause problems</a>",
	"prompt-access-request": " is trying to view your stream. Allow them?",
	"confirm-reload-user": "Are you sure you wish to reload this user's browser?",
	"webrtc-is-blocked": "⚠ This browser has either blocked WebRTC or does not support it.\n\nThis site will not work without it.\n\nDisable any browser extensions or privacy settings that may be blocking WebRTC, or try a different browser.",
	"not-clean-session": "Video effects or canvas rendering failed.\n\nCheck to ensure any remotely hosted images are cross-origin allowed.",
	"ios-no-screen-share": "Sorry, but your iOS browser does not support screen-sharing.\n\nPlease see <a href='https://docs.vdo.ninja/guides/screen-share-your-iphone-ipad' target='_blank'>this guide</a> for an alternative method to do so.",
	"mobile-no-screen-share": "Sorry, your mobile browser does not support screen-sharing.\n\nThe <a href='https://docs.vdo.ninja/getting-started/native-mobile-app-versions' target='_blank'>The native apps</a> do offer basic support for it though.",
	"no-screen-share-supported": "Sorry, your browser does not support screen-sharing.\n\nPlease use the desktop versions of Firefox or Chrome instead.",
	"no-screen-share-supported-firefox": "Sorry, your browser does not support screen-sharing.\n\nYour Firefox settings may be configured to block it or you've accessed the site insecurely.",
	"speech-not-suppoted": "⚠ Speech Recognition is not supported by this browser",
	"blue-yeti-tip": "<i>Tip:</i> Blue Yeti microphones may experience issues being overly loud. <a href='https://support.google.com/chrome/thread/7542181?hl=en&msgid=79691143'>Please see here</a> for a solution or disable auto-gain in VDO.Ninja.",
	"sample-rate-too-high": "Your audio playback device has its sample rate set very high. If having audio issues, try using 48-kHz instead.",
	"site-not-responsive": "<h3>Notice: The system cannot be accessed or is currently slow to respond.</h3>\nIf a routing issue, try adding <i title='or try visiting https://proxy.vdo.ninja/'>&proxy</i> to the URL; you can also try <i>https://proxy.vdo.ninja</i> or a VPN if the service is blocked in your country.\n\nIf the main service is down, a backup version is also available here: <i>https://backup.vdo.ninja</i>\n\nContact steve@seguin.email for added help.\n\nThis service requires the use of Websockets over port 443.",
	"no-audio-source-detected": "No audio source was detected.<br><br>Please see <a href='https://docs.vdo.ninja/common-errors-and-known-issues/cant-capture-an-applications-audio-when-screen-sharing' target='_blank'>the documention</a> for a guide on how to capture application-based audio.",
	"viewer-count": "Total outbound p2p connections of this remote stream",
	"enter-url-for-widget": "Enter a URL for a page to embed as a sidebar",
	"director-password": "Enter the main director's password",
	"vision-disabled": "The Director has disabled your vision temporarily<br /><br ><center><i style='font-size:500%;' class='las la-eye-slash'></i></center>",
	"invalid-remote-code": "Invalid remote control code.\n\nUse the field below to try again with a different passcode.",
	"invalid-remote-code-obs": "Invalid remote control code.\n\nThe remote OBS system needs a matching passcode set using &remote.\n\nSee the documentation for help..",
	"request-rejected-obs": "The request was rejected.\n\nThe remote OBS system needs a matching passcode set using &remote.\n\nSee the documentation for help.",
	"remote-token-rejected": "The remote request failed; the &remote token did not match or the remote user does not allow remote control.",
	"remote-control-failed": "The remote control request failed.",
	"remote-peer-connected": "Remote peer connected to video stream.\n\nConnection to handshake server being killed on request. This increases security, but the peer will not be able to reconnect automatically on connection failure.\n\nPress OK to start the stream!",
	"director-denied": "⚠️ The main director denied you as a co-director\n\nYou will only be able to preview streams; you will not be able to control or change anything.",
	"only-main-director": "Only the main director can transfer this guest",
	"request-failed": "The request failed; you can't apply this action",
	"tokens-did-not-match": "The remote request failed; the remote token did not match or the remote user does not allow remote control.",
	"token-not-director": "The request failed; the remote user did not recognize you as the director.\n\nRefreshing may help in some cases, if you are indeed the director.",
	"approved-as-director": "The director approved you as a co-director",
	"you-are-a-codirector": "You are a co-director of this room; you have partial director control assigned to you.",
	"this-is-you": "This is you, a co-director.<br />You are also a performer.",
	"preview-meshcast-disabled": "You can't adjust the preview bitrate for Meshcast or WHIP-based streams",
	"no-network": "Network connection lost 🤷‍♀️❌📶",
	"no-network-details": "Network connection lost. 🤷‍♀️❌📶\n\nHave you lost your Internet connection?",
	"enter-password-if-desired": "Enter a password if provided, otherwise just click Cancel",
	"your-screenshare": "Your screenshare",
	"your-camera": "Your camera",
	"accept-inbound-caller": "Accept the inbound telephone caller?",
	"disable-video": "Disable Video",
	"show-more-options": "Show more options",
	"system-default": "System Default"
};

function getTranslation(key) {
	// when using this, instead of miniTranslate, if the user changes the language, it might not update. Used mainly when you don't want any HTML (<span data-translate>) being including in the translation
	if (translation.innerHTML && key in translation.innerHTML) {
		// these are the proper translations
		return translation.innerHTML[key];
	} else if (key in miscTranslations) {
		// i guess these can be transitioned to innerHTML
		return miscTranslations[key];
	} else {
		warnlog("misc translation not found");
		return key.replaceAll("-", " "); //
	}
}

if (typeof session === "undefined") {
	// make sure to init the WebRTC if not exists.
	var session = WebRTC.Media;
	session.streamID = session.generateStreamID();
	errorlog("Serious error: WebRTC session didn't load in time");
}

try {
	// this is just in case orientationchange gets removed..
	if (!window.onorientationchange && screen.orientation) {
		// onorientationchange is deprecated.
		window.onorientationchange = function () {
			log("screen.orientation triggered.. but nothing linked");
		};
		screen.orientation.addEventListener("change", window.onorientationchange);
	}
} catch (e) {
	errorlog(e);
}

(function (w) {
	w.URLSearchParams =
		w.URLSearchParams ||
		function (searchString) {
			var self = this;
			searchString = searchString.replace("??", "?");
			self.searchString = searchString;
			self.get = function (name) {
				var results = new RegExp("[?&]" + name + "=([^&#]*)").exec(self.searchString);
				if (results == null) {
					return null;
				} else {
					return decodeURI(results[1]) || 0;
				}
			};
		};
})(window);

var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
var urlParams = new URLSearchParams(urlEdited);

if (urlParams.has("invite") || urlParams.has("i") || urlParams.has("code")) {
	session.decodeInvite(urlParams.get("invite") || urlParams.get("i") || urlParams.get("code"));
} else if (urlParams.has("preset")) {
	try {
		let preset = urlParams.get("preset") || "1"; // default to preset 1 if none provided
		let xhttp = new XMLHttpRequest();
		xhttp.open("GET", "presets.json", false); // blocking
		xhttp.setRequestHeader("Content-Type", "application/json"); // expecting json response
		xhttp.send();
		if (xhttp.status === 200) {
			const response = JSON.parse(xhttp.responseText);
			let presetString = "";

			if (Array.isArray(response)) {
				let index = (parseInt(preset) || 1) - 1;
				presetString = response[index];
			} else if (typeof response === "object" && response !== null) {
				presetString = response[preset];
			}
			if (!presetString.startsWith("?") || presetString.startsWith("&")) {
				presetString = "?" + presetString;
			}

			session.preset = presetString;

			let newURL = presetString + "&" + urlParams.toString();
			newURL = newURL.replace(/\?/g, "&");
			newURL = newURL.replace(/\&/, "?");
			urlParams = new URLSearchParams(newURL);

			if (urlParams.has("invite") || urlParams.has("i") || urlParams.has("code")) {
				session.decodeInvite(urlParams.get("invite") || urlParams.get("i") || urlParams.get("code"));
			}
		} else {
			errorlog(xhttp.statusTex);
		}
	} catch (error) {
		errorlog(error);
	}
}

if (session.decrypted) {
	session.decrypted = session.decrypted + urlEdited.replace("?", "&");
	session.decrypted = session.decrypted.replace(/\?/g, "&");
	session.decrypted = session.decrypted.replace(/\&/, "?");
	urlParams = new URLSearchParams(session.decrypted);
	//session.decrypted = true;
} else if (urlEdited !== window.location.search) {
	warnlog(window.location.search + " changed to " + urlEdited);
	if (!session.nohistory){
		window.history.pushState({ path: urlEdited.toString() }, "", urlEdited.toString());
	}
}
delete urlEdited;

var isIFrame = false;
if (parent && window.location !== window.parent.location) {
	isIFrame = true;
}

function mapToAll(targets, callback, parentElement = document) {
	// js helper
	if (!targets) {
		return;
	}
	if (!parentElement) {
		return;
	}
	const target = parentElement.querySelectorAll(targets);
	for (let i = 0; i < target.length; i++) {
		callback(target[i]);
	}
}

function changeParam(url, paramName, paramValue) {
	paramName = paramName.replace("?", "");
	var qind = url.indexOf("?");
	url = url.replace("?", "&");
	var params = url.substring(qind + 1).split("&");
	var query = "";
	var match = false;
	for (var i = 0; i < params.length; i++) {
		var tokens = params[i].split("=");
		var name = tokens[0];
		var value = "";
		if (tokens.length > 1 && tokens[1] !== "") {
			value = tokens[1];
		}

		if (name == paramName) {
			if (match) {
				continue;
			} // already matched the first time.
			match = true;
			value = paramValue;
		}
		if (value !== "") {
			value = "=" + value;
		}

		if (query == "") {
			query = "?" + name + value;
		} else {
			query = query + "&" + name + value;
		}
	}
	return url.substring(0, qind) + query;
}

function saveRoom(ele) {
	//this.title = "Quick load settings stored locally";
	session.sticky = true;
	ele.parentNode.removeChild(ele);
	setStorage("permission", "yes");
	setStorage("settings", encodeURI(window.location.href), 999);
}

function updateURL(param, force = false, cleanUrl = false) {
	if (session.decrypted) {
		return;
	}

	param = param.replace("?", "");
	var para = param.split("=");
	if (cleanUrl) {
		if (history.pushState) {
			var href = new URL(cleanUrl);
			if (para.length == 1) {
				href = changeParam(cleanUrl, para[0], "");
			} else {
				href = changeParam(cleanUrl, para[0], para[1]);
			}
			log("--" + href.toString());
			if (!session.nohistory){
				window.history.pushState({ path: href.toString() }, "", href.toString());
			}
		}
	} else if (!urlParams.has(para[0])) {
		// don't need to replace as it doesn't exist.
		if (history.pushState) {
			var href = window.location.href;
			href = href.replace("??", "?");
			var arr = href.split("?");
			var newurl;
			if (arr.length > 1 && arr[1] !== "") {
				newurl = href + "&" + param;
			} else {
				newurl = href + "?" + param;
			}
			if (!session.nohistory){
				window.history.pushState({ path: newurl.toString() }, "", newurl.toString());
			}
		}
	} else if (force) {
		if (history.pushState) {
			var href = new URL(window.location.href);
			if (para.length == 1) {
				href = changeParam(window.location.href, para[0], "");
			} else {
				href = changeParam(window.location.href, para[0], para[1]);
			}
			log("---" + href.toString());
			if (!session.nohistory){
				window.history.pushState({ path: href.toString() }, "", href.toString());
			}
		}
	}
	if (session.sticky) {
		setStorage("settings", encodeURI(window.location.href), 999);
	}

	urlParams = new URLSearchParams(window.location.search);

	if (session.preset) {
		let newURL = session.preset + "&" + urlParams.toString();
		newURL = newURL.replace(/\?/g, "&");
		newURL = newURL.replace(/\&/, "?");
		urlParams = new URLSearchParams(newURL);
	}
}

/* function changeGuestSettings(ele){
	var eles = ele.querySelectorAll('[data-param]');
	var UUID = ele.dataset.UUID;
	var settings = {};
	for (var i = 0;i< eles.length; i++){
		if (eles[i].tagName.toLowerCase() == "input"){
			if (eles[i].checked===true){
				settings[eles[i].dataset.param] = true;
			} else if (eles[i].checked===false){
				settings[eles[i].dataset.param] = false;
			} else {
				settings[eles[i].dataset.param] = eles[i].value;
			}
		}
	}
	warnlog(settings);
	
	if (!settings.changepassword){
		delete settings.password;
	}
	
	delete settings.changepassword;
	
	if (!settings.changeroom){
		// send Migration message
		delete settings.roomid;
	} 
	delete settings.roomid;
	delete settings.changeroom;
	
	warnlog(UUID);
	var msg = {};
	msg.changeParams = settings;
	session.sendRequest(msg, UUID);
	closeModal();
} */

// proper room migration needs to happen; in sync.
// updateMixer after settings changed
// password needs to be special cased
// room shouldn't be sent

function applyNewParams(changeParams) {
	for (var key in changeParams) {
		session[key] = changeParams[key];
		log(key);
	}
	log(changeParams);
	updateMixer();
}

function submitDebugLog(msg = false) {
	try {
		if (navigator.userAgent) {
			var _,
				userAgent = navigator.userAgent;
			appendDebugLog({ userAgent: userAgent });
		}
		if (navigator.platform) {
			appendDebugLog({ userAgent: navigator.platform });
		}
	} catch (e) {}
	window.focus();
	var res = confirm(getTranslation("submit-error-report"));
	if (res) {
		var request = new XMLHttpRequest();

		var recordResults = session.streamID + "_" + parseInt(Date.now());
		request.open("POST", "https://reports.vdo.ninja/?name=" + recordResults); //  php, well, whatever.
		if (!session.cleanOutput) {
			warnUser("Report any details of your bug report to steve@seguin.email, along with the following link: <a target='_blank' onclick='copyFunction(this, event)' href='https://reports.vdo.ninja/?name=" + recordResults + "'>https://reports.vdo.ninja/?name=" + recordResults + "</a>", false, false);
		}
		console.log("Report any details of your bug report to steve@seguin.email, along with the following ID: " + recordResults);

		request.send(JSON.stringify(errorReport));
		errorReport = [];
		if (document.getElementById("reportbutton")) {
			getById("reportbutton").classList.add("hidden");
		}
	}
}

function URLFromFiles(files) {
	const promises = files.map(file => fetch(file).then(response => response.text()));

	return Promise.all(promises).then(texts => {
		const text = texts.join("");
		const blob = new Blob([text], { type: "application/javascript" });

		return URL.createObjectURL(blob);
	});
}

function detectCPUSupport() {
	let cpuThreads = navigator.hardwareConcurrency;
	if (cpuThreads) {
		return cpuThreads + " threads";
	}
	return false;
}

function detectGPUSupport() {
	try {
		const gl = document.createElement("canvas").getContext("webgl");

		if (!gl) {
			return false;
		}

		if (!Firefox) {
			try {
				const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); // chrome
				if (debugInfo) {
					return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
				}
			} catch (e) {}
		}

		try {
			return gl.getParameter(gl.RENDERER) || false; // firefox
		} catch (e) {}
	} catch (e) {}
	return false;
}

function isOperaGX() {
	return (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(" OPR/75") >= 0;
}

function isSamsungASeries() {
	return navigator.userAgent.includes("; SM-A") || false;
}

function getChromiumVersion() {
	var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
	return raw ? parseInt(raw[2], 10) : false;
}

function getiOSVersion() {
	try {
		var agent = navigator.userAgent;
		var start = agent.indexOf("OS ");
		if ((agent.indexOf("iPhone") > -1 || agent.indexOf("iPad") > -1) && start > -1) {
			return window.Number(agent.substr(start + 3, 3).replace("_", "."));
		}
		return 0;
	} catch (e) {
		return 0;
	}
	return 0;
}

function safariVersion() {
	var ver = 0;
	try {
		ver = navigator.appVersion.split("Version/");
		if (ver.length > 1) {
			ver = ver[1].split(" Safari");
		}
		if (ver.length > 1) {
			ver = ver[0].split(".");
		}
		if (ver.length > 1) {
			ver = parseInt(ver[0]);
		} else {
			ver = 0;
		}
	} catch (e) {
		return 0;
	}
	return ver;
}

function isIntelMac() {
  // Check if it's a Mac but not Apple Silicon
  if (macOS && navigator.userAgent.indexOf("Intel") >= 0) {
    return true;
  }
  return false;
}

function judgePerformance(){
  try {
    if (SafariVersion && SafariVersion >= 17 && (iOS || iPad)) { // iphone xr or newer
      return 0;
    }
    
    const cores = typeof navigator.hardwareConcurrency === 'number' ? navigator.hardwareConcurrency : 0;
    
    if (isIntelMac()) {
      if (cores < 6) { // yes. they are that bad.
        return 2;
      } else {
        return 1;
      }
    }
    
    if (session.mobile && (cores>=4)){ // assume hardware encoded acceleration
      return 0;
    }
    
    if (!cores){
      return 1;
    } else if (cores < 4 ){
      return 2;
    } else if (cores>8){
      return 0;
    }
    return 1
  } catch (e) {
    return 1; // 99% safe default
  }
}

try {
	var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); // used by main.js also
	var iPad = navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);

	var macOS = navigator.userAgent.indexOf("Mac OS X") != -1;
	macOS = macOS && !(iOS || iPad);
	var Firefox = navigator.userAgent.indexOf("Firefox") >= 0;
	if (Firefox) {
		Firefox = parseInt(navigator.userAgent.split("irefox/").pop()) || true;
	}
	var Android = navigator.userAgent.toLowerCase().indexOf("android") > -1; //&& ua.indexOf("mobile");
	var ChromiumVersion = getChromiumVersion();
	var OperaGx = isOperaGX();
	var SafariVersion = safariVersion() || getiOSVersion(); // I should rename this to webkit

	if (iOS || iPad) {
		// iOS doesn't yet allow actual browsers, cause it's abusing its duopoly.
		if (SafariVersion) {
			if (Firefox) {
				Firefox = false; //  I should rename this to gecko
			}
			if (ChromiumVersion) {
				ChromiumVersion = false; // I should rename this to chromium
			}
		}
	}
	var SamsungASeries = isSamsungASeries();
	var isVingester = navigator.userAgent.indexOf("Vingester") >= 0;

	var gpgpuSupport = detectGPUSupport(); // graphics ; not supported on ios
	log(gpgpuSupport);

	var cpuSupport = detectCPUSupport(); // thread count ; supported on ios
	log(cpuSupport);

	var iPhone12Up = false;
	
	var isMELD = false;
	if (typeof navigator!== 'undefined' && navigator.userAgent && navigator.userAgent.includes("Meld/")) {
		isMELD = true;
	}

	if (iOS && !iPad) {
		if (window.devicePixelRatio.toFixed(2) >= 3 && window.screen.height > 800 && window.screen.width != 414) {
			// for reference, https://www.ios-resolution.com/
			iPhone12Up = true; // iPhone SE is left out.
		}
	}
	session.quality_wb = judgePerformance(); // try to estimate what resolution to use for encoding when not in a room.
	
	if (session.quality_room < session.quality_wb){
		session.quality_room = session.quality_wb;
	}
	
} catch (e) {
	errorlog(e);
}
const needsLegacyWakeLock = () => {
	try {
		if ('wakeLock' in navigator) {
			if (Firefox) { // using your existing Firefox detection
				return true;
			}
			if ((iOS || iPad) && SafariVersion < 16.4) { // using your existing iOS/iPad/SafariVersion detection
				return true;
			}
			return false;
		}
	} catch(e){}
    
    return true; // No Wake Lock API, need legacy keep alive for mobile
};

if (session.audioCtx && session.audioCtx.sampleRate && session.audioCtx.sampleRate > 192000) {
	console.warn("Your audio playback device has a very high sample-rate set of " + session.audioCtx.sampleRate + "-Hz. If having audio problems, lower to at least 192000-Hz, but preferably 48000-Hz.");
	if (!session.cleanOutput) {
		miniTranslate(getById("audioTipContextSR"), "sample-rate-too-high");
		getById("audioTipSR").classList.remove("hidden");
	}
}

if (isVingester) {
	console.warn("If Vingester isn't able to capture audio, get a fixed version of Vingester from here: https://github.com/steveseguin/vingester/releases/");
}

function isAlphaNumeric(str) {
	var code, i, len;
	for (i = 0, len = str.length; i < len; i++) {
		code = str.charCodeAt(i);
		if (
			!(code > 47 && code < 58) && // numeric (0-9)
			!(code > 64 && code < 91) && // upper alpha (A-Z)
			!(code > 96 && code < 123)
		) {
			// lower alpha (a-z)
			return false;
		}
	}
	return true;
}

function convertStringToArrayBufferView(str) {
	var bytes = new Uint8Array(str.length);
	for (var iii = 0; iii < str.length; iii++) {
		bytes[iii] = str.charCodeAt(iii);
	}
	return bytes;
}

function toHexString(byteArray) {
	return Array.prototype.map
		.call(byteArray, function (byte) {
			return ("0" + (byte & 0xff).toString(16)).slice(-2);
		})
		.join("");
}
function toByteArray(hexString) {
	var result = [];
	for (var i = 0; i < hexString.length; i += 2) {
		result.push(parseInt(hexString.substr(i, 2), 16));
	}
	return new Uint8Array(result);
}

function playAllVideos() {
	if (session.firstPlayTriggered && session.audioCtx.state == "suspended") {
		//  added oct 9th 2022
		try {
			session.audioCtx.resume();
		} catch (e) {
			warnlog(e);
		}
	}

	for (var i in session.rpcs) {
		if (session.rpcs[i].whip) {
			continue;
		}
		try {
			if (session.rpcs[i].videoElement) {
				log("I: " + i);
				if (session.rpcs[i].videoElement.paused) {
					setTimeout(
						function (UUID) {
							session.rpcs[UUID].videoElement
								.play()
								.then(_ => {
									log("playing 3 ");
									if (session.audioEffects === true || session.pushLoudness) {
										log("updateIncomingAudioElement('" + UUID + "')");
										updateIncomingAudioElement(UUID);
									}
								})
								.catch(errorlog);
						},
						0,
						i
					);
				} else if (session.audioEffects === true || session.pushLoudness) {
					updateIncomingAudioElement(i);
					log("updateIncomingAudioElement('" + i + "')");
				}
			}
		} catch (e) {
			errorlog(e);
		}
	}
}

var videoElements = Array.from(document.querySelectorAll("video"));
var audioElements = Array.from(document.querySelectorAll("audio"));
var mediaStreamCounter = 0;

function createMediaStream() {
	mediaStreamCounter += 1;
	return new MediaStream();
}

function deleteOldMedia() {
	log("CHECKING FOR OLD MEDIA");
	var i = videoElements.length;
	while (i--) {
		//if ((videoElements[i].id == "videosource") || (videoElements[i].id == "previewWebcam")){continue;} // exclude this one, for safety reasons. (Also, iOS safari blanks the video if streams are detached and moved between video elements)
		if (videoElements[i].isConnected === false) {
			if (videoElements[i].srcObject == null || (videoElements[i].srcObject && videoElements[i].srcObject.active === false)) {
				if (videoElements[i].dataset && videoElements[i].dataset.UUID) {
					if (videoElements[i].dataset.UUID in session.rpcs) {
						continue;
					} // still active, so lets not delete it.
				}
				videoElements[i].pause();
				videoElements[i].removeAttribute("id");
				videoElements[i].removeAttribute("src"); // empty source
				videoElements[i].load();
				videoElements[i].remove();
				videoElements[i] = null;
				videoElements.splice(i, 1);
			}
		}
	}
	i = audioElements.length;
	while (i--) {
		if (audioElements[i].isConnected === false) {
			if (audioElements[i].srcObject == null || (audioElements[i].srcObject && audioElements[i].srcObject.active === false)) {
				if (audioElements[i].dataset && audioElements[i].dataset.UUID) {
					if (audioElements[i].dataset.UUID in session.rpcs) {
						continue;
					} // still active, so lets not delete it.
				}
				audioElements[i].pause();
				audioElements[i].id = null;
				audioElements[i].removeAttribute("src"); // empty source
				audioElements[i].load();
				audioElements[i].remove();
				audioElements[i] = null;
				audioElements.splice(i, 1);
			}
		}
	}
}

function createAudioElement() {
	try {
		deleteOldMedia();
	} catch (e) {
		errorlog(e);
	}
	var a = document.createElement("audio");
	audioElements.push(a);
	return a;
}

function compare_deltas(a, b) {
	var aa = a.delta || 0;
	var bb = b.delta || 0;
	if (aa > bb) {
		return 1;
	}
	if (aa < bb) {
		return -1;
	}
	return 0;
}

async function fetchWithTimeout(URL, timeout = 8000) {
	// ref: https://dmitripavlutin.com/timeout-fetch-request/
	try {
		const controller = new AbortController();
		const timeout_id = setTimeout(() => controller.abort(), timeout);
		const response = await fetch(URL, { ...{ timeout: timeout }, signal: controller.signal });
		clearTimeout(timeout_id);
		return response;
	} catch (e) {
		errorlog(e);
		return await fetch(URL); // iOS 11.x/12.0
	}
}

function createVideoElement() {
	try {
		deleteOldMedia();
	} catch (e) {
		errorlog(e);
	}
	var v = document.createElement("video");
	videoElements.push(v);
	if (typeof session.volume == "number") {
		v.volume = session.volume; // setting default volume
		log("setting volume to manual");
	}

	return v;
}

function getTimezone() {
	if (session.tz !== false) {
		return session.tz;
	}
	const stdTimezoneOffset = () => {
		var jan = new Date(0, 1);
		var jul = new Date(6, 1);
		return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
	};
	var today = new Date();
	const isDstObserved = today => {
		return today.getTimezoneOffset() < stdTimezoneOffset();
	};
	if (isDstObserved(today)) {
		return today.getTimezoneOffset() + 60;
	} else {
		return today.getTimezoneOffset();
	}
}

function promptUser(eleId, UUID = null) {
	if (session.beepToNotify) {
		playtone();
	}
	if (document.getElementById("modalBackdrop")) {
		getById("promptModal").innerHTML = ""; // Delete modal
		getById("promptModal").remove();
		getById("modalBackdrop").innerHTML = ""; // Delete modal
		getById("modalBackdrop").remove();
	}

	zindex = document.querySelectorAll("#promptModal").length + document.querySelectorAll(".alertModal").length;
	modalTemplate = `<div id="promptModal" style="z-index:${zindex + 2}">	
		<div class="promptModalInner">
			<span class='modalClose' onclick="closeModal()">×</span>
			<span id='promptModalMessage'></span>
		</div>
	</div>
	<div id="modalBackdrop" style="z-index:${zindex + 1}"></div>`;
	document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end

	getById("promptModalMessage").innerHTML = getById(eleId).innerHTML;
	miniTranslate(getById("promptModal"));

	if (UUID) {
		getById("promptModalMessage").dataset.UUID = UUID;
	}

	document.getElementById("modalBackdrop").addEventListener("click", closeModal);

	getById("promptModal").addEventListener("click", function (e) {
		e.stopPropagation();
		return false;
	});
}

async function delay(ms) {
	return await new Promise((resolve, reject) => {
		setTimeout(resolve, ms);
	});
}

var Prompts = {};
async function promptAlt(inputText, block = false, asterix = false, value = false, time = false, recording = false, hotkey = false, field = null) {
	var result = null;
	if (session.beepToNotify) {
		playtone();
	}
	await new Promise((resolve, reject) => {
		var promptID = "pid_" + Math.random().toString(36).substr(2, 9);
		Prompts[promptID] = {};
		Prompts[promptID].resolve = resolve;
		Prompts[promptID].reject = reject;

		var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length;

		if (block) {
			var backdropClass = "opaqueBackdrop";
		} else {
			var backdropClass = "modalBackdrop";
		}

		inputText = "<span style='font-size:1.2em'>" + inputText.replace("\n", "</span><br /><span>") + "</span>";
		inputText = inputText.replace(/\n/g, "<br />");
		var type = "text";
		if (asterix) {
			type = "password";
		}

		if (time) {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage' style='margin:20px'>${inputText}</span>
						<span style="margin:20px";>
							<input id="input_${promptID}" title="minutes" autocorrect="off" autocapitalize="none" data-pid="${promptID}" value="0" style="width:50px;padding:4px;margin:4px;" min="0" type="number" /> minutes, 
							<input id="input_${promptID}_sec"  title="seconds" autocorrect="off" autocapitalize="none" data-pid="${promptID}" value="0"  style="width:50px;padding:4px;margin:4px;" max="59" min="0" type="number" /> seconds
							<br /><br />
							<input type='checkbox' id="countup_${promptID}" style="margin-left:30px" title="Count up from 0 instead" data-pid="${promptID}" /> Count up from zero instead
						</span>
						<br /><br />
						<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 30px" data-translate='ok'>✔ OK</button>
						<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
						<br /><br />
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else if (recording) {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage'>${inputText}</span>
						<span style="margin:20px";>
							<input id="input_${promptID}" autocorrect="off" autocapitalize="none" data-pid="${promptID}"  type="${type}" class="largeTextEntry" />
							<input id="input_${promptID}_gdrive" title="Upload to Google Drive also"  data-pid="${promptID}" type="checkbox" /> Upload to your Google Drive
							<input id="input_${promptID}_dbx" title="Upload to Dropbox also"  data-pid="${promptID}" type="checkbox" /> Upload to your Drop Box
						</span>
						<br /><br />
						<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
						<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else if (field && field.type === "file") {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">    
				<div class="promptModalInner">
					<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
					<span class='promptModalMessage'>${inputText}</span>
					<div style="margin:20px">
						<p style="margin:10px 5px">${field.placeholder}</p>
						<input id="input_${promptID}" type="file" accept="${field.accept}" data-pid="${promptID}" style="width:100%" />
						<div id="preview_${promptID}" style="max-width:200px; max-height:200px; margin:10px auto;"></div>
					</div>
					<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
					<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
				</div>
			</div>
			<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else if (hotkey) {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage'>${inputText}</span>
						<br />
						<span style="margin:20px";>
							<label title="Choose a hotkey for Hold-to-Talk.">Press-to-Talk Hot-key</label>
							<input type="text" id="input_${promptID}"  placeholder="press a key here" style="padding-left: 7px;" onkeypress="event.preventDefault;event.stopPropagation();return false;" onkeyup="event.preventDefault;event.stopPropagation();return false;" onkeydown="setHotKey();"/>
							<button onclick="setHotKey(false);" style="margin: 0 0 0 4px; border-radius: 5px; padding: 3px 3px;">Clear</button>
							<button id="submit_${promptID}" data-pid="${promptID}" style="min-width:70px; color:black; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 10px;" data-translate='ok'>Close</button>
						</span>
						<br />
						<br />
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else if (field && field.type === "select") {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
				<div class="promptModalInner">
					<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
					<span class='promptModalMessage'>${inputText}</span>
					<div style="margin:20px">
						<p style="margin:10px 5px">${field.placeholder}</p>
						<select id="select_${promptID}" data-pid="${promptID}" style="width:100%;padding:8px;margin-bottom:10px">
							<option value="">-- Select --</option>
							${field.options.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
						</select>
						<input id="input_${promptID}" style="display:none" autocorrect="off" autocapitalize="none" data-pid="${promptID}" type="text" class="largeTextEntry" placeholder="${field.placeholder}" />
					</div>
					<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
					<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
				</div>
			</div>
			<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else if (field && field.placeholder) {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage'>${inputText}</span>
						<input id="input_${promptID}" autocorrect="off" autocapitalize="none" data-pid="${promptID}" placeholder="${field.placeholder}" type="${type}" class="largeTextEntry" />
						<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
						<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage'>${inputText}</span>
						<input id="input_${promptID}" autocorrect="off" autocapitalize="none" data-pid="${promptID}"  type="${type}" class="largeTextEntry" />
						<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
						<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		}

		document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end
		
		// Only add select listener if we have a select field
		if (field && field.type === "select") {
			document.getElementById(`select_${promptID}`).addEventListener("change", (e) => {
				const input = document.getElementById(`input_${promptID}`);
				if (e.target.value === "[Custom]") {
					input.style.display = "block";
					input.focus();
				} else {
					input.style.display = "none";
					result = e.target.value;
				}
			});
		}

		document.getElementById("input_" + promptID).focus();

		if (value !== false) {
			if (time) {
				document.getElementById("input_" + promptID).value = parseInt(value / 60);
				document.getElementById("input_" + promptID + "_sec").value = parseInt(value) % 60;
			} else {
				document.getElementById("input_" + promptID).value = value;
			}
		}

		if (time) {
			document.getElementById("input_" + promptID).addEventListener("keyup", function (event) {
				if (event.key === "Enter") {
					document.getElementById("input_" + promptID + "_sec").focus();
				}
			});
			document.getElementById("input_" + promptID + "_sec").addEventListener("keyup", function (event) {
				if (event.key === "Enter") {
					document.getElementById("submit_" + promptID).focus();
				}
			});
			document.getElementById("countup_" + promptID).addEventListener("click", function (event) {
				if (document.getElementById("countup_" + promptID).checked) {
					document.getElementById("input_" + promptID).disabled = true;
					document.getElementById("input_" + promptID + "_sec").disabled = true;
				} else {
					document.getElementById("input_" + promptID).disabled = false;
					document.getElementById("input_" + promptID + "_sec").disabled = false;
					delete document.getElementById("input_" + promptID).disabled;
					delete document.getElementById("input_" + promptID + "_sec").disabled;
				}
			});
		} else if (field && field.type === "file") {
			document.getElementById(`input_${promptID}`).addEventListener("change", async (e) => {
				const file = e.target.files[0];
				if (file) {
					result = file;
					if (file.type.startsWith('image/')) {
						const preview = document.getElementById(`preview_${promptID}`);
						const reader = new FileReader();
						reader.onload = (e) => {
							preview.innerHTML = `<img src="${e.target.result}" style="max-width:100%; max-height:200px;">`;
						};
						reader.readAsDataURL(file);
					}
				}
			});
		} else {
			document.getElementById("input_" + promptID).addEventListener("keyup", function (event) {
				if (event.key === "Enter") {
					var pid = event.target.dataset.pid;
					if (field && field.type === "select") {
						const selectEl = document.getElementById(`select_${pid}`);
						if (selectEl.value === "[Custom]") {
							result = document.getElementById(`input_${pid}`).value;
						} else {
							result = selectEl.value;
						}
					} else {
						result = document.getElementById("input_" + pid).value;
					}
					document.getElementById("modal_" + pid).remove();
					document.getElementById("modalBackdrop_" + pid).remove();
					Prompts[pid].resolve();
				}
			});
		}

		try {
			document.getElementById("submit_" + promptID).addEventListener("click", async function (event) {
				var pid = event.target.dataset.pid;
				if (time) {
					result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60;
					if (document.getElementById("countup_" + promptID).checked) {
						result = 0;
					}
				} else if (field && field.type === "select") {
					const selectEl = document.getElementById(`select_${pid}`);
					if (selectEl.value === "[Custom]") {
						result = document.getElementById(`input_${pid}`).value;
					} else {
						result = selectEl.value;
					}
				} else if (field && field.type === "file") {
					if (result) {
						await handleImageUpload(result, field);
					}
				} else {
					result = document.getElementById("input_" + pid).value;
				}
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
			});
		} catch (e) {}
		
		try {
			document.getElementById("cancel_" + promptID).addEventListener("click", function (event) {
				var pid = event.target.dataset.pid;
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
			});
		} catch (e) {}

		try {
			document.getElementById("close_" + promptID).addEventListener("click", function (event) {
				var pid = event.target.dataset.pid;
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
			});
		} catch (e) {}

		getById("modal_" + promptID).addEventListener("click", function (e) {
			e.stopPropagation();
			return false;
		});
		miniTranslate(getById("modal_" + promptID));
		return;
	});
	return result;
}

async function promptRecordingOptions(inputText, block = false, defaultOptions = {}) {
    var result = null;
	// console.log(defaultOptions);
	var defaultVideoBitrate = 6000;
	var defaultAudioBitrate = 80;
	
	if (defaultOptions.audioOnly && !defaultOptions.usePCM){
		defaultAudioBitrate = defaultOptions.bitrate || 80;
	} else if (!defaultOptions.audioOnly){
		defaultVideoBitrate = defaultOptions.bitrate || 6000;
	}
	
    if (session.beepToNotify) {
        playtone();
    }
	
	// Helper functions for bitrate controls
	function updateBitrateControls(promptID) {
		const audioOnly = document.getElementById(`audioOnly_${promptID}`).checked;
		const usePCM = document.getElementById(`usePCM_${promptID}`).checked;
		const pcmContainer = document.getElementById(`audioFormatContainer_${promptID}`);
		const slider = document.getElementById(`bitrateSlider_${promptID}`);
		const value = document.getElementById(`bitrateValue_${promptID}`);
		const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`);
		
		// PCM option always available
		pcmContainer.style.opacity = '1';
		pcmContainer.style.pointerEvents = 'auto';
		bitrateLabel.parentNode.style.opacity = "1";
		bitrateLabel.parentNode.style.pointerEvents = 'auto';
		//document.getElementById(`usePCM_${promptID}`).disabled = false;
		
		// Update bitrate controls based on mode
		if (audioOnly && usePCM) {
			// Audio-only PCM mode
			bitrateLabel.textContent = "Uncompressed PCM16 audio";
			bitrateLabel.parentNode.style.opacity = "0.5";
			bitrateLabel.parentNode.style.pointerEvents = 'none';
			slider.disabled = true;
			value.disabled = true;
			slider.value = "";
			value.value = "";
		} else if (audioOnly && !usePCM) {
			// Audio-only OPUS mode
			bitrateLabel.textContent = "Audio recording bitrate (OPUS):";
			slider.disabled = false;
			value.disabled = false;
			slider.min = "1";
			slider.max = "128";
			slider.value = defaultAudioBitrate;
			slider.step = "1";
			value.value = defaultAudioBitrate;
		} else if (!audioOnly && usePCM) {
			// Video + PCM audio mode
			bitrateLabel.textContent = "Video bitrate (with PCM16 audio):";
			slider.disabled = false;
			value.disabled = false;
			slider.min = "50";
			slider.max = "10000";
			slider.value = defaultVideoBitrate;
			slider.step = "100";
			value.value = defaultVideoBitrate;
		} else {
			// Video + OPUS audio mode
			bitrateLabel.textContent = "Recording Bitrate:";
			slider.disabled = false;
			value.disabled = false;
			slider.min = "50";
			slider.max = "10000";
			slider.value = defaultVideoBitrate;
			slider.step = "100";
			value.value = defaultVideoBitrate;
		}
		
		updateBitrateQuality(promptID);
	}

	function updateBitrateValue(promptID) {
		const value = document.getElementById(`bitrateSlider_${promptID}`).value;
		document.getElementById(`bitrateValue_${promptID}`).value = value;
		updateBitrateQuality(promptID);
	}

	function updateBitrateSlider(promptID) {
		const value = document.getElementById(`bitrateValue_${promptID}`).value;
		document.getElementById(`bitrateSlider_${promptID}`).value = value;
		updateBitrateQuality(promptID);
	}

	function updateBitrateQuality(promptID) {
		const audioOnly = document.getElementById(`audioOnly_${promptID}`).checked;
		const usePCM = document.getElementById(`usePCM_${promptID}`).checked;
		const value = parseInt(document.getElementById(`bitrateValue_${promptID}`).value);
		const qualitySpan = document.getElementById(`bitrateQuality_${promptID}`);
		
		if (audioOnly && usePCM) {
			qualitySpan.textContent = "N/A";
			return;
		}
		
		if (audioOnly) {
			if (value < 32) qualitySpan.textContent = "Low";
			else if (value >= 80) qualitySpan.textContent = "High";
			else qualitySpan.textContent = "Medium";
			defaultAudioBitrate = value;
		} else {
			if (value < 2000) qualitySpan.textContent = "Low";
			else if (value >= 6000) qualitySpan.textContent = "High";
			else qualitySpan.textContent = "Medium";
			defaultVideoBitrate = value;
		}
	}
    
    await new Promise((resolve, reject) => {
        var promptID = "pid_" + Math.random().toString(36).substr(2, 9);
        Prompts[promptID] = {};
        Prompts[promptID].resolve = resolve;
        Prompts[promptID].reject = reject;

        var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length;
        var backdropClass = block ? "opaqueBackdrop" : "modalBackdrop";

		inputText = "<h2>Recording setup</h2><br>" + inputText.replace(/\n/g, "<br />");

        const modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">    
            <div class="promptModalInner">
                <span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
                <span class='promptModalMessage'>${inputText}</span>
                
                <!-- Recording Options -->
                <div class="recording-options" style="margin: 20px; padding:5px 0;">
                    <label title="Note: the audio might not be received as PCM; this just records it in PCM." id="audioFormatContainer_${promptID}" style="display: block; margin: 20px;">
                        <input title="Note: the audio might not be received as PCM; this just records it in PCM."  type="checkbox" id="usePCM_${promptID}" data-pid="${promptID}">
                        Use PCM audio format
                    </label>
					
					<label style="display: block; margin: 20px;">
                        <input type="checkbox" id="audioOnly_${promptID}" data-pid="${promptID}">
                        Audio-only recording
                    </label>
                </div>

                <!-- Bitrate Controls -->
                <div class="bitrate-controls" style="margin: 30px 0 10px 0;">
                    <label id="bitrateLabel_${promptID}">Recording Bitrate:</label>
                    <div style="display: flex; align-items: center; gap: 10px;">
                        <input type="range" 
                            id="bitrateSlider_${promptID}" 
                            min="50" 
                            max="10000" 
                            value="${defaultVideoBitrate || 6000}"
                            step="100"
                            style="flex: 1;"
                        >
                        <input type="number" 
                            id="bitrateValue_${promptID}" 
                            value="${defaultVideoBitrate || 6000}"
                            style="width: 80px;"
                        >
                        <span style="min-width:75px;" id="bitrateQuality_${promptID}">High</span>
                    </div>
                </div>

                <!-- Buttons -->
                <div style="display: flex; gap: 10px; margin-top: 20px;">
                    <button style="font-size: large;flex:1;" alt="Start recording" title="Start recording the video to disk" id="submit_${promptID}" data-pid="${promptID}" 
                        style="flex: 1; padding: 11px 8px">✔ Start Recording</button>
                    <button style="font-size: large;flex:1;" alt="Cancel" title="Cancel the recording" id="cancel_${promptID}" data-pid="${promptID}" 
                        style="flex: 1; padding: 11px 8px">❌ Cancel</button>
                </div>
            </div>
        </div>start record
        <div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;

        document.body.insertAdjacentHTML("beforeend", modalTemplate);

        // Set default values if provided
        if (defaultOptions.audioOnly) {
            document.getElementById(`audioOnly_${promptID}`).checked = true;
            updateBitrateControls(promptID);
        }
        if (defaultOptions.usePCM) {
            document.getElementById(`usePCM_${promptID}`).checked = true;
			if (defaultOptions.audioOnly) {
				const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`);
				bitrateLabel.textContent = "Uncompressed PCM16 audio";
				bitrateLabel.parentNode.style.opacity = "0.5";
				bitrateLabel.parentNode.style.pointerEvents = 'none';
			} else {
				const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`);
				bitrateLabel.textContent = "Video bitrate (with PCM16 audio):";
			}
			
        }

        // Add event listeners
        document.getElementById(`audioOnly_${promptID}`).addEventListener("change", () => updateBitrateControls(promptID));
        document.getElementById(`usePCM_${promptID}`).addEventListener("change", () => updateBitrateControls(promptID));
        document.getElementById(`bitrateSlider_${promptID}`).addEventListener("input", () => updateBitrateValue(promptID));
        document.getElementById(`bitrateValue_${promptID}`).addEventListener("change", () => updateBitrateSlider(promptID));

        // Submit handler
        document.getElementById(`submit_${promptID}`).addEventListener("click", function(event) {
            const pid = event.target.dataset.pid;
            result = {
                audioOnly: document.getElementById(`audioOnly_${pid}`).checked,
                usePCM: document.getElementById(`usePCM_${pid}`).checked,
                bitrate: parseInt(document.getElementById(`bitrateValue_${pid}`).value)
            };
            document.getElementById(`modal_${pid}`).remove();
            document.getElementById(`modalBackdrop_${pid}`).remove();
            Prompts[pid].resolve();
        });

        // Cancel handler
        document.getElementById(`cancel_${promptID}`).addEventListener("click", function(event) {
            const pid = event.target.dataset.pid;
            result = null;
            document.getElementById(`modal_${pid}`).remove();
            document.getElementById(`modalBackdrop_${pid}`).remove();
            Prompts[pid].resolve();
        });

        // Close handler
        document.getElementById(`close_${promptID}`).addEventListener("click", function(event) {
            const pid = event.target.dataset.pid;
            result = null;
            document.getElementById(`modal_${pid}`).remove();
            document.getElementById(`modalBackdrop_${pid}`).remove();
            Prompts[pid].resolve();
        });

        // Stop propagation on modal click
        document.getElementById(`modal_${promptID}`).addEventListener("click", function(e) {
            e.stopPropagation();
            return false;
        });

        // Translate if needed
        miniTranslate(document.getElementById(`modal_${promptID}`));
    });

    return result;
}


async function promptAltRecord(inputText, block = false, asterix = false, value = false) {
	var result = null;
	if (session.beepToNotify) {
		playtone();
	}
	await new Promise((resolve, reject) => {
		var promptID = "pid_" + Math.random().toString(36).substr(2, 9);
		Prompts[promptID] = {};
		Prompts[promptID].resolve = resolve;
		Prompts[promptID].reject = reject;

		var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length;

		if (block) {
			var backdropClass = "opaqueBackdrop";
		} else {
			var backdropClass = "modalBackdrop";
		}

		inputText = "<span style='font-size:1.2em'>" + inputText.replace("\n", "</span><br /><span>") + "</span>";
		inputText = inputText.replace(/\n/g, "<br />");
		var type = "text";
		if (asterix) {
			type = "password";
		}

		if (time) {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage' style='margin:20px'>${inputText}</span>
						<span style="margin:20px";>
							<input id="input_${promptID}" title="minutes" autocorrect="off" autocapitalize="none" data-pid="${promptID}" value="0" style="width:50px;padding:4px;margin:4px;" min="0" type="number" /> minutes, 
							<input id="input_${promptID}_sec"  title="seconds" autocorrect="off" autocapitalize="none" data-pid="${promptID}" value="0"  style="width:50px;padding:4px;margin:4px;" max="59" min="0" type="number" /> seconds
							<br /><br />
							<input type='checkbox' id="countup_${promptID}" style="margin-left:30px" title="Count up from 0 instead" data-pid="${promptID}" /> Count up from zero instead
						</span>
						<br /><br />
						<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 30px" data-translate='ok'>✔ OK</button>
						<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
						<br /><br />
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else if (recording) {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage'>${inputText}</span>
						<span style="margin:20px";>
							<input id="input_${promptID}" autocorrect="off" autocapitalize="none" data-pid="${promptID}"  type="${type}" class="largeTextEntry" />
							<input id="input_${promptID}_gdrive" title="Upload to Google Drive also"  data-pid="${promptID}" type="checkbox" /> Upload to your Google Drive
							<input id="input_${promptID}_dbx" title="Upload to Dropbox also"  data-pid="${promptID}" type="checkbox" /> Upload to your Drop Box
						</span>
						<br /><br />
						<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
						<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else if (hotkey) {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage'>${inputText}</span>
						<br />
						<span style="margin:20px";>
							<label title="Choose a hotkey for Hold-to-Talk.">Press-to-Talk Hot-key</label>
							<input type="text" id="input_${promptID}"  placeholder="press a key here" style="padding-left: 7px;" onkeypress="event.preventDefault;event.stopPropagation();return false;" onkeyup="event.preventDefault;event.stopPropagation();return false;" onkeydown="setHotKey();"/>
							<button onclick="setHotKey(false);" style="margin: 0 0 0 4px; border-radius: 5px; padding: 3px 3px;">Clear</button>
							<button id="submit_${promptID}" data-pid="${promptID}" style="min-width:70px; color:black; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 10px;" data-translate='ok'>Close</button>
						</span>
						<br />
						<br />
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		} else {
			modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
					<div class="promptModalInner">
						<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
						<span class='promptModalMessage'>${inputText}</span>
						<input id="input_${promptID}" autocorrect="off" autocapitalize="none" data-pid="${promptID}"  type="${type}" class="largeTextEntry" />
						<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
						<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
					</div>
				</div>
				<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;
		}

		document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end

		document.getElementById("input_" + promptID).focus();

		if (value !== false) {
			if (time) {
				document.getElementById("input_" + promptID).value = parseInt(value / 60);
				document.getElementById("input_" + promptID + "_sec").value = parseInt(value) % 60;
			} else {
				document.getElementById("input_" + promptID).value = value;
			}
		}

		if (time) {
			document.getElementById("input_" + promptID).addEventListener("keyup", function (event) {
				if (event.key === "Enter") {
					document.getElementById("input_" + promptID + "_sec").focus();
				}
			});
			document.getElementById("input_" + promptID + "_sec").addEventListener("keyup", function (event) {
				if (event.key === "Enter") {
					document.getElementById("submit_" + promptID).focus();
				}
			});
			document.getElementById("countup_" + promptID).addEventListener("click", function (event) {
				if (document.getElementById("countup_" + promptID).checked) {
					document.getElementById("input_" + promptID).disabled = true;
					document.getElementById("input_" + promptID + "_sec").disabled = true;
				} else {
					document.getElementById("input_" + promptID).disabled = false;
					document.getElementById("input_" + promptID + "_sec").disabled = false;
					delete document.getElementById("input_" + promptID).disabled;
					delete document.getElementById("input_" + promptID + "_sec").disabled;
				}
			});
		} else {
			document.getElementById("input_" + promptID).addEventListener("keyup", function (event) {
				if (event.key === "Enter") {
					var pid = event.target.dataset.pid;
					result = document.getElementById("input_" + pid).value;
					document.getElementById("modal_" + pid).remove();
					document.getElementById("modalBackdrop_" + pid).remove();
					Prompts[pid].resolve();
				}
			});
		}

		try {
			document.getElementById("submit_" + promptID).addEventListener("click", function (event) {
				var pid = event.target.dataset.pid;
				if (time) {
					result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60;

					if (document.getElementById("countup_" + promptID).checked) {
						result = 0;
					}
				} else {
					result = document.getElementById("input_" + pid).value;
				}

				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();

				Prompts[pid].resolve();
			});
		} catch (e) {}

		try {
			document.getElementById("cancel_" + promptID).addEventListener("click", function (event) {
				var pid = event.target.dataset.pid;
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
			});
		} catch (e) {}

		try {
			document.getElementById("close_" + promptID).addEventListener("click", function (event) {
				var pid = event.target.dataset.pid;
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
			});
		} catch (e) {}

		getById("modal_" + promptID).addEventListener("click", function (e) {
			e.stopPropagation();
			return false;
		});
		miniTranslate(getById("modal_" + promptID));
		return;
	});
	return result;
}

async function promptRecord() {
	var result = null;
	if (session.beepToNotify) {
		playtone();
	}
	await new Promise((resolve, reject) => {
		var promptID = "pid_" + Math.random().toString(36).substr(2, 9);
		Prompts[promptID] = {};
		Prompts[promptID].resolve = resolve;
		Prompts[promptID].reject = reject;

		var zindex = 1030 + document.querySelectorAll(".promptModal").length;

		var backdropClass = "modalBackdrop";

		var inputText = "RECORD SETTINGS";

		inputText = "<span style='font-size:1.2em'>" + inputText.replace("\n", "</span><br /><span>") + "</span>";
		inputText = inputText.replace(/\n/g, "<br />");
		var type = "text";

		modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
				<div class="promptModalInner">
					<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
					<span class='promptModalMessage'>${inputText}</span>
					<span style="margin:20px";>
						<input id="input_${promptID}" autocorrect="off" autocapitalize="none" data-pid="${promptID}"  type="${type}" class="largeTextEntry" />
						<input id="input_${promptID}_gdrive" title="Upload to Google Drive also"  data-pid="${promptID}" type="checkbox" /> Upload to your Google Drive
						<input id="input_${promptID}_dbx" title="Upload to Dropbox also"  data-pid="${promptID}" type="checkbox" /> Upload to your Drop Box
					</span>
					<br /><br />
					<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
					<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
				</div>
			</div>
			<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;

		document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end

		var gdrive = getById(`input_${promptID}_gdrive`);
		gdrive.onchange = async function () {
			if (this.checked) {
				this.uploadLink = await session.gdrive.startResumableUpload();
			} else {
				this.uploadLink = null;
			}
		};

		document.getElementById("input_" + promptID).focus();

		document.getElementById("input_" + promptID).addEventListener("keyup", function (event) {
			if (event.key === "Enter") {
				var pid = event.target.dataset.pid;
				result = document.getElementById("input_" + pid).value;
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
			}
		});

		try {
			document.getElementById("submit_" + promptID).addEventListener("click", function (event) {
				var pid = event.target.dataset.pid;
				if (time) {
					result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60;

					if (document.getElementById("countup_" + promptID).checked) {
						result = 0;
					}
				} else {
					result = document.getElementById("input_" + pid).value;
				}

				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();

				Prompts[pid].resolve();
			});
		} catch (e) {}

		try {
			document.getElementById("cancel_" + promptID).addEventListener("click", function (event) {
				var pid = event.target.dataset.pid;
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
			});
		} catch (e) {}

		try {
			document.getElementById("close_" + promptID).addEventListener("click", function (event) {
				var pid = event.target.dataset.pid;
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
			});
		} catch (e) {}

		getById("modal_" + promptID).addEventListener("click", function (e) {
			e.stopPropagation();
			return false;
		});
		miniTranslate(getById("modal_" + promptID));
		return;
	});
	return result;
}

async function promptTransfer(value = null, bcmode = null, updateurl = null, queueMode = null) {
	var result = { roomid: null };
	if (session.beepToNotify) {
		playtone();
	}
	await new Promise((resolve, reject) => {
		var promptID = "pid_" + Math.random().toString(36).substr(2, 9);
		Prompts[promptID] = {};
		Prompts[promptID].resolve = resolve;
		Prompts[promptID].reject = reject;

		var zindex = 30 + document.querySelectorAll(".promptModal").length;
		var backdropClass = "modalBackdrop";

		var inputText = "<span style='font-size:1.2em'>" + getTranslation("transfer-guest-to-room").replace("\n", "</span><br /><span>") + "</span>";
		inputText = inputText.replace(/\n/g, "<br />");

		modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
				<div class="promptModalInner">
					<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
					<span class='promptModalMessage'>${inputText}</span>
					<input id="input_${promptID}" data-pid="${promptID}"  type="text" autocorrect="off" autocapitalize="none" class="largeTextEntry" />
					<span class='promptModalLabel'><input id="private_${promptID}" data-pid="${promptID}"  type="checkbox" title="Note: this won't work fully if using obfuscated links" /> Allow the guest to rejoin the transfer room on their own</span>
					<span class='promptModalLabel'><input id="broadcast_${promptID}" data-pid="${promptID}"  type="checkbox" /> Guest will arrive in the new room in <i>broadcast</i> mode</span>
					<span class='promptModalLabel'><input id="queued_${promptID}" data-pid="${promptID}"  type="checkbox" /> Guest will arrive in the new room in <i>queue</i> mode</span>
					<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
					<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
				</div>
			</div>
			<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;

		document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end

		document.getElementById("input_" + promptID).focus();

		if (value !== null) {
			document.getElementById("input_" + promptID).value = value;
		}

		if (bcmode !== null) {
			document.getElementById("broadcast_" + promptID).checked = bcmode;
		}
		if (queueMode !== null) {
			document.getElementById("queued_" + promptID).checked = queueMode;
		}

		if (updateurl !== null) {
			document.getElementById("private_" + promptID).checked = updateurl;
		}

		document.getElementById("input_" + promptID).addEventListener("keyup", function (event) {
			if (event.key === "Enter") {
				var pid = event.target.dataset.pid;
				var room = document.getElementById("input_" + pid).value;
				var updateurl = document.getElementById("private_" + pid).checked;
				var broadcast = document.getElementById("broadcast_" + pid).checked;
				var queue = document.getElementById("queued_" + pid).checked;
				document.getElementById("modal_" + pid).remove();
				document.getElementById("modalBackdrop_" + pid).remove();
				Prompts[pid].resolve();
				result = { roomid: room, updateurl: updateurl, broadcast: broadcast, queue: queue };
			}
		});

		document.getElementById("submit_" + promptID).addEventListener("click", function (event) {
			var pid = event.target.dataset.pid;
			var room = document.getElementById("input_" + pid).value;
			var updateurl = document.getElementById("private_" + pid).checked;
			var broadcast = document.getElementById("broadcast_" + pid).checked;
			var queue = document.getElementById("queued_" + pid).checked;
			document.getElementById("modal_" + pid).remove();
			document.getElementById("modalBackdrop_" + pid).remove();
			Prompts[pid].resolve();
			result = { roomid: room, updateurl: updateurl, broadcast: broadcast, queue: queue };
		});

		document.getElementById("cancel_" + promptID).addEventListener("click", function (event) {
			var pid = event.target.dataset.pid;
			document.getElementById("modal_" + pid).remove();
			document.getElementById("modalBackdrop_" + pid).remove();
			Prompts[pid].resolve();
		});

		document.getElementById("close_" + promptID).addEventListener("click", function (event) {
			var pid = event.target.dataset.pid;
			document.getElementById("modal_" + pid).remove();
			document.getElementById("modalBackdrop_" + pid).remove();
			Prompts[pid].resolve();
		});

		getById("modal_" + promptID).addEventListener("click", function (e) {
			e.stopPropagation();
			return false;
		});
		miniTranslate(getById("modal_" + promptID));
		return;
	});
	return result;
}

function youveBeenTransferred() {
	getChatMessage(getTranslation("you-have-been-transferred"), (label = false), (director = false), (overlay = true)); // "you-have-been-transferred"
	getById("head2").innerHTML = getTranslation("room-changed"); // not sure this is right??

	miniTranslate(getById("head2"), "room-changed");

	if (session.director) {
		getById("head4").innerHTML = getTranslation("you-are-no-longer-a-co-director"); //"You are no longer a co-director as you were transferred."; //
	}

	if (session.label) {
		document.title = session.label + " - " + getTranslation("transferred");
	} else {
		document.title = getTranslation("transferred");
	}

	hideHomeCheck();
}

function youveBeenActivated() {
	if (session.queueType == 3) {
		// session.queue is now set to false; hold without video
		//getById("overlayMsgs").innerText = "";
		closeModal(false, "123");
		for (var UUID in session.pcs) {
			if (session.pcs[UUID].needsPublishing) {
				session.initialPublish(UUID); // let's starts publishing now
			}
		}
	} else if (session.queueType == 4) {
		// hold with video
		//getById("overlayMsgs").innerText = "";
		closeModal(false, "123");
	}

	getChatMessage(getTranslation("you-have-been-activated"), (label = false), (director = false), (overlay = true));
	hideHomeCheck();
}

function youreWaitingToBeActivated() {
	// getChatMessage( getTranslation("you-not-yet-activated"), label = false, director = false, overlay = true);
	warnUser(getTranslation("you-not-yet-activated"), false, false, 123);
	hideHomeCheck();
}

async function confirmAlt(inputText, block = false) {
	var result = null;
	if (session.beepToNotify) {
		playtone();
	}
	await new Promise((resolve, reject) => {
		var promptID = "pid_" + Math.random().toString(36).substr(2, 9);
		Prompts[promptID] = {};
		Prompts[promptID].resolve = resolve;
		Prompts[promptID].reject = reject;

		var zindex = 33 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length;

		if (block) {
			var backdropClass = "opaqueBackdrop";
		} else {
			var backdropClass = "modalBackdrop";
		}

		inputText = "<span style='font-size:1.2em'>" + inputText.replace("\n", "</span><br /><span>") + "</span>";
		inputText = inputText.replace(/\n/g, "<br />");

		modalTemplate = `<div id="modal_${promptID}" class="promptModal" style="z-index:${zindex + 2}">	
				<div class="promptModalInner">
					<span id="close_${promptID}" class='modalClose' data-pid="${promptID}">×</span>
					<span class='promptModalMessage' style='margin: 0 0 15px 0;'>${inputText}</span>
					<button id="submit_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0 0 0 55px;" data-translate='ok'>✔ OK</button>
					<button id="cancel_${promptID}" data-pid="${promptID}" style="width:120px; background-color: #fff; position: relative;border: 1px solid #999; margin: 0;" data-translate='cancel'>❌ Cancel</button>
				</div>
			</div>
			<div id="modalBackdrop_${promptID}" class="${backdropClass}" style="z-index:${zindex + 1}"></div>`;

		document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end

		document.getElementById("submit_" + promptID).focus();

		document.getElementById("submit_" + promptID).addEventListener("click", function (event) {
			var pid = event.target.dataset.pid;
			result = true;
			getById("modalBackdrop_" + pid).remove();
			getById("modal_" + pid).remove();
			Prompts[pid].resolve();
		});

		document.getElementById("cancel_" + promptID).addEventListener("click", function (event) {
			var pid = event.target.dataset.pid;
			getById("modalBackdrop_" + pid).remove();
			getById("modal_" + pid).remove();
			Prompts[pid].resolve();
		});

		document.getElementById("close_" + promptID).addEventListener("click", function (event) {
			var pid = event.target.dataset.pid;
			getById("modalBackdrop_" + pid).remove();
			getById("modal_" + pid).remove();
			Prompts[pid].resolve();
		});

		getById("modal_" + promptID).addEventListener("click", function (e) {
			e.stopPropagation();
			return false;
		});
		miniTranslate(getById("modal_" + promptID));
		return;
	});
	return result;
}

var modalTimeout = null;
function warnUser(message, timeout = false, sanitize = true, modalID = false) {
	// Allows for multiple alerts to stack better.
	// Every modal and backdrop has an increasing z-index
	// to block the previous modal
	if (!message) {
		return;
	}

	if (document.getElementById("modalBackdrop")) {
		getById("alertModal").innerHTML = ""; // Delete modal
		getById("alertModal").remove();
		getById("modalBackdrop").innerHTML = ""; // Delete modal
		getById("modalBackdrop").remove();
	}

	zindex = 31 + document.querySelectorAll(".alertModal").length + document.querySelectorAll(".promptModal").length;
	try {
		if (sanitize) {
			message = sanitizeChat(message, 2000);
		}
		message = message.replace(/\n/g, "<br />");
	} catch (e) {
		errorlog(message);
	}

	if (!modalID) {
		modalID = Math.floor(Math.random() * 999) + 1000;
	}

	modalTemplate = `<div class="alertModal" id="alertModal" data-modalID=${modalID} style="z-index:${zindex + 2}">	
		<div class="alertModalInner">
			<span class='modalClose' onclick="closeModal()">×</span>
			<span id="alertModalMessage" class='alertModalMessage'>${message}</span>
		</div>
	</div>
	<div id="modalBackdrop" style="z-index:${zindex + 1}"></div>`;
	document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end

	document.getElementById("modalBackdrop").addEventListener("click", closeModal);

	clearTimeout(modalTimeout);
	if (timeout) {
		modalTimeout = setTimeout(closeModal, timeout, false, modalID);
	}
	getById("alertModal").addEventListener("click", function (e) {
		e.stopPropagation();
		return false;
	});
	return modalID;
}
function closeModal(ele = false, modalID = false) {
	if (modalID && !ele) {
		if (!document.querySelector("#alertModal[data-modalID='" + modalID + "']")) {
			return;
		}
	}

	clearTimeout(modalTimeout);
	try {
		getById("modalBackdrop").innerHTML = ""; // Delete modal
		getById("modalBackdrop").remove();
		getById("alertModal").innerHTML = ""; // Delete modal
		getById("alertModal").remove();
		getById("promptModal").innerHTML = ""; // Delete modal
		getById("promptModal").remove();

		query(".modalBackdrop").innerHTML = ""; // Delete modal
		query(".modalBackdrop").remove();

		if (ele && ele.innerHTML && ele.remove) {
			ele.innerHTML = ""; // Delete specific modal
			ele.remove();
		}
	} catch (e) {
		warnlog(e);
	}
	if (session.timeoutTriggered) {
		session.warnUserTriggered = false;
	}
}

var sanitizeStreamID = function (streamID) {
	streamID = streamID.trim();

	if (streamID.length < 1) {
		streamID = session.generateStreamID(8);
		if (!session.cleanOutput) {
			warnUser(getTranslation("no-streamID-provided") + streamID, false, false);
		}
	}
	var streamID_sanitized = streamID.replace(/[\W]+/g, "_");
	if (streamID !== streamID_sanitized) {
		if (!session.cleanOutput) {
			warnUser(getTranslation("alphanumeric-only"), false, false);
		}
	}
	if (streamID_sanitized.length > 64) {
		streamID_sanitized = streamID_sanitized.substring(0, 70); // leave room for salting
		if (!session.cleanOutput) {
			warnUser(getTranslation("stream-id-too-long"), false, false);
		}
	}
	return streamID_sanitized;
};

var checkStrength = function (string) {
	var matcher = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{7,30}$/;
	if (string.match(matcher)) {
		return true;
	} else if (string.length > 20) {
		return true;
	} else {
		return false;
	}
};

var checkStrengthRoom = function () {
	var result1 = checkStrength(getById("videoname1").value);
	var result2 = getById("passwordRoom").value.length;
	var target = getById("securityLevelRoom");
	target.style.display = "block";
	if (result1) {
		if (result2) {
			target.innerHTML = "<span style='color:green'>" + getTranslation("share-with-trusted") + "</span>";
		} else {
			target.innerHTML = "<span style='color:#e67202;'>" + getTranslation("pass-recommended") + "</span>";
		}
	} else {
		target.innerHTML = "<span style='color:red'>" + getTranslation("insecure-room-name") + "</span> " + getTranslation("allowed-chars") + ": <i>A-Z, a-z, 0-9, _</i>";
	}
};

var emojiShortCodes = {
	":joy:": "😂",
	":heart:": "❤️",
	":heart_eyes:": "😍",
	":sob:": "😭",
	":blush:": "😊",
	":unamused:": "😒",
	":two_hearts:": "💕",
	":weary:": "😩",
	":ok_hand:": "👌",
	":pensive:": "😔",
	":smirk:": "😏",
	":grin:": "😁",
	":wink:": "😉",
	":thumbsup:": "👍",
	":pray:": "🙏",
	":relieved:": "😌",
	":notes:": "🎶",
	":flushed:": "😳",
	":raised_hands:": "🙌",
	":see_no_evil:": "🙈",
	":cry:": "😢",
	":sunglasses:": "😎",
	":v:": "✌️",
	":eyes:": "👀",
	":sweat_smile:": "😅",
	":sparkles:": "✨",
	":sleeping:": "😴",
	":smile:": "😄",
	":purple_heart:": "💜",
	":broken_heart:": "💔",
	":blue_heart:": "💙",
	":confused:": "😕",
	":disappointed:": "😞",
	":yum:": "😋",
	":neutral_face:": "😐",
	":sleepy:": "😪",
	":clap:": "👏",
	":cupid:": "💘",
	":heartpulse:": "💗",
	":kiss:": "💋",
	":point_right:": "👉",
	":scream:": "😱",
	":fire:": "🔥",
	":rage:": "😡",
	":smiley:": "😃",
	":tada:": "🎉",
	":tired_face:": "😫",
	":camera:": "📷",
	":rose:": "🌹",
	":muscle:": "💪",
	":skull:": "💀",
	":sunny:": "☀️",
	":yellow_heart:": "💛",
	":triumph:": "😤",
	":laughing:": "😆",
	":sweat:": "😓",
	":point_left:": "👈",
	":grinning:": "😀",
	":mask:": "😷",
	":green_heart:": "💚",
	":wave:": "👋",
	":persevere:": "😣",
	":heartbeat:": "💓",
	":crown:": "👑",
	":innocent:": "😇",
	":headphones:": "🎧",
	":confounded:": "😖",
	":angry:": "😠",
	":grimacing:": "😬",
	":star2:": "🌟",
	":gun:": "🔫",
	":raising_hand:": "🙋",
	":thumbsdown:": "👎",
	":dancer:": "💃",
	":musical_note:": "🎵",
	":no_mouth:": "😶",
	":dizzy:": "💫",
	":fist:": "✊",
	":point_down:": "👇",
	":no_good:": "🙅",
	":boom:": "💥",
	":tongue:": "👅",
	":poop:": "💩",
	":cold_sweat:": "😰",
	":gem:": "💎",
	":ok_woman:": "🙆",
	":pizza:": "🍕",
	":joy_cat:": "😹",
	":leaves:": "🍃",
	":sweat_drops:": "💦",
	":penguin:": "🐧",
	":zzz:": "💤",
	":walking:": "🚶",
	":airplane:": "✈️",
	":balloon:": "🎈",
	":star:": "⭐",
	":ribbon:": "🎀",
	":worried:": "😟",
	":underage:": "🔞",
	":fearful:": "😨",
	":hibiscus:": "🌺",
	":microphone:": "🎤",
	":open_hands:": "👐",
	":ghost:": "👻",
	":palm_tree:": "🌴",
	":nail_care:": "💅",
	":alien:": "👽",
	":bow:": "🙇",
	":cloud:": "☁",
	":soccer:": "⚽",
	":angel:": "👼",
	":dancers:": "👯",
	":snowflake:": "❄️",
	":point_up:": "☝️",
	":rainbow:": "🌈",
	":gift_heart:": "💝",
	":gift:": "🎁",
	":beers:": "🍻",
	":anguished:": "😧",
	":earth_africa:": "🌍",
	":movie_camera:": "🎥",
	":anchor:": "⚓",
	":zap:": "⚡",
	":runner:": "🏃",
	":sunflower:": "🌻",
	":bouquet:": "💐",
	":dog:": "🐶",
	":moneybag:": "💰",
	":herb:": "🌿",
	":couple:": "👫",
	":fallen_leaf:": "🍂",
	":tulip:": "🌷",
	":birthday:": "🎂",
	":cat:": "🐱",
	":coffee:": "☕",
	":dizzy_face:": "😵",
	":point_up_2:": "👆",
	":open_mouth:": "😮",
	":hushed:": "😯",
	":basketball:": "🏀",
	":ring:": "💍",
	":astonished:": "😲",
	":hear_no_evil:": "🙉",
	":dash:": "💨",
	":cactus:": "🌵",
	":hotsprings:": "♨️",
	":telephone:": "☎️",
	":maple_leaf:": "🍁",
	":princess:": "👸",
	":massage:": "💆",
	":love_letter:": "💌",
	":trophy:": "🏆",
	":blossom:": "🌼",
	":lips:": "👄",
	":fries:": "🍟",
	":doughnut:": "🍩",
	":frowning:": "😦",
	":ocean:": "🌊",
	":bomb:": "💣",
	":cyclone:": "🌀",
	":rocket:": "🚀",
	":umbrella:": "☔",
	":couplekiss:": "💏",
	":lollipop:": "🍭",
	":clapper:": "🎬",
	":pig:": "🐷",
	":smiling_imp:": "😈",
	":imp:": "👿",
	":bee:": "🐝",
	":kissing_cat:": "😽",
	":anger:": "💢",
	":santa:": "🎅",
	":earth_asia:": "🌏",
	":football:": "🏈",
	":guitar:": "🎸",
	":panda_face:": "🐼",
	":strawberry:": "🍓",
	":smirk_cat:": "😼",
	":banana:": "🍌",
	":watermelon:": "🍉",
	":snowman:": "⛄",
	":smile_cat:": "😸",
	":eggplant:": "🍆",
	":crystal_ball:": "🔮",
	":calling:": "📲",
	":iphone:": "📱",
	":partly_sunny:": "⛅",
	":warning:": "⚠️",
	":scream_cat:": "🙀",
	":baby:": "👶",
	":feet:": "🐾",
	":footprints:": "👣",
	":beer:": "🍺",
	":wine_glass:": "🍷",
	":video_camera:": "📹",
	":rabbit:": "🐰",
	":smoking:": "🚬",
	":peach:": "🍑",
	":snake:": "🐍",
	":turtle:": "🐢",
	":cherries:": "🍒",
	":kissing:": "😗",
	":frog:": "🐸",
	":milky_way:": "🌌",
	":closed_book:": "📕",
	":candy:": "🍬",
	":hamburger:": "🍔",
	":bear:": "🐻",
	":tiger:": "🐯",
	":icecream:": "🍦",
	":pineapple:": "🍍",
	":ear_of_rice:": "🌾",
	":syringe:": "💉",
	":tv:": "📺",
	":pill:": "💊",
	":octopus:": "🐙",
	":grapes:": "🍇",
	":smiley_cat:": "😺",
	":cd:": "💿",
	":cocktail:": "🍸",
	":cake:": "🍰",
	":video_game:": "🎮",
	":lipstick:": "💄",
	":whale:": "🐳",
	":cookie:": "🍪",
	":dolphin:": "🐬",
	":loud_sound:": "🔊",
	":man:": "👨",
	":monkey:": "🐒",
	":books:": "📚",
	":guardsman:": "💂",
	":loudspeaker:": "📢",
	":scissors:": "✂️",
	":girl:": "👧",
	":mortar_board:": "🎓",
	":baseball:": "⚾️",
	":woman:": "👩",
	":fireworks:": "🎆",
	":stars:": "🌠",
	":mushroom:": "🍄",
	":pouting_cat:": "😾",
	":left_luggage:": "🛅",
	":high_heel:": "👠",
	":dart:": "🎯",
	":swimmer:": "🏊",
	":key:": "🔑",
	":bikini:": "👙",
	":family:": "👪",
	":pencil2:": "✏",
	":elephant:": "🐘",
	":droplet:": "💧",
	":seedling:": "🌱",
	":apple:": "🍎",
	":dollar:": "💵",
	":book:": "📖",
	":haircut:": "💇",
	":computer:": "💻",
	":bulb:": "💡",
	":boy:": "👦",
	":tangerine:": "🍊",
	":sunrise:": "🌅",
	":poultry_leg:": "🍗",
	":shaved_ice:": "🍧",
	":bird:": "🐦",
	":eyeglasses:": "👓",
	":goat:": "🐐",
	":older_woman:": "👵",
	":new_moon:": "🌑",
	":customs:": "🛃",
	":house:": "🏠",
	":full_moon:": "🌕",
	":lemon:": "🍋",
	":baby_bottle:": "🍼",
	":spaghetti:": "🍝",
	":wind_chime:": "🎐",
	":fish_cake:": "🍥",
	":nose:": "👃",
	":pig_nose:": "🐽",
	":fish:": "🐟",
	":koala:": "🐨",
	":ear:": "👂",
	":shower:": "🚿",
	":bug:": "🐛",
	":ramen:": "🍜",
	":tophat:": "🎩",
	":fuelpump:": "⛽",
	":horse:": "🐴",
	":watch:": "⌚",
	":monkey_face:": "🐵",
	":baby_symbol:": "🚼",
	":sparkler:": "🎇",
	":corn:": "🌽",
	":tennis:": "🎾",
	":battery:": "🔋",
	":wolf:": "🐺",
	":moyai:": "🗿",
	":cow:": "🐮",
	":mega:": "📣",
	":older_man:": "👴",
	":dress:": "👗",
	":link:": "🔗",
	":chicken:": "🐔",
	":whale2:": "🐋",
	":bento:": "🍱",
	":pushpin:": "📌",
	":dragon:": "🐉",
	":hamster:": "🐹",
	":golf:": "⛳",
	":surfer:": "🏄",
	":mouse:": "🐭",
	":blue_car:": "🚙",
	":bread:": "🍞",
	":cop:": "👮",
	":tea:": "🍵",
	":bike:": "🚲",
	":rice:": "🍚",
	":radio:": "📻",
	":baby_chick:": "🐤",
	":sheep:": "🐑",
	":lock:": "🔒",
	":green_apple:": "🍏",
	":racehorse:": "🐎",
	":fried_shrimp:": "🍤",
	":volcano:": "🌋",
	":rooster:": "🐓",
	":inbox_tray:": "📥",
	":wedding:": "💒",
	":sushi:": "🍣",
	":ice_cream:": "🍨",
	":tomato:": "🍅",
	":rabbit2:": "🐇",
	":beetle:": "🐞",
	":bath:": "🛀",
	":no_entry:": "⛔",
	":crocodile:": "🐊",
	":dog2:": "🐕",
	":cat2:": "🐈",
	":hammer:": "🔨",
	":meat_on_bone:": "🍖",
	":shell:": "🐚",
	":poodle:": "🐩",
	":stew:": "🍲",
	":jeans:": "👖",
	":honey_pot:": "🍯",
	":unlock:": "🔓",
	":black_nib:": "✒",
	":snowboarder:": "🏂",
	":white_flower:": "💮",
	":necktie:": "👔",
	":womens:": "🚺",
	":ant:": "🐜",
	":city_sunset:": "🌇",
	":dragon_face:": "🐲",
	":snail:": "🐌",
	":dvd:": "📀",
	":shirt:": "👕",
	":game_die:": "🎲",
	":dolls:": "🎎",
	":8ball:": "🎱",
	":bus:": "🚌",
	":custard:": "🍮",
	":camel:": "🐫",
	":curry:": "🍛",
	":hospital:": "🏥",
	":bell:": "🔔",
	":pear:": "🍐",
	":door:": "🚪",
	":saxophone:": "🎷",
	":church:": "⛪",
	":bicyclist:": "🚴",
	":dango:": "🍡",
	":office:": "🏢",
	":rowboat:": "🚣",
	":womans_hat:": "👒",
	":mans_shoe:": "👞",
	":love_hotel:": "🏩",
	":mount_fuji:": "🗻",
	":handbag:": "👜",
	":hourglass:": "⌛",
	":trumpet:": "🎺",
	":school:": "🏫",
	":cow2:": "🐄",
	":toilet:": "🚽",
	":pig2:": "🐖",
	":violin:": "🎻",
	":credit_card:": "💳",
	":ferris_wheel:": "🎡",
	":bowling:": "🎳",
	":barber:": "💈",
	":purse:": "👛",
	":rat:": "🐀",
	":date:": "📅",
	":ram:": "🐏",
	":tokyo_tower:": "🗼",
	":kimono:": "👘",
	":ship:": "🚢",
	":mag_right:": "🔎",
	":mag:": "🔍",
	":fire_engine:": "🚒",
	":police_car:": "🚓",
	":black_joker:": "🃏",
	":package:": "📦",
	":calendar:": "📆",
	":horse_racing:": "🏇",
	":tiger2:": "🐅",
	":boot:": "👢",
	":ambulance:": "🚑",
	":boar:": "🐗",
	":pound:": "💷",
	":ox:": "🐂",
	":rice_ball:": "🍙",
	":sandal:": "👡",
	":tent:": "⛺",
	":seat:": "💺",
	":taxi:": "🚕",
	":briefcase:": "💼",
	":newspaper:": "📰",
	":circus_tent:": "🎪",
	":mens:": "🚹",
	":flashlight:": "🔦",
	":foggy:": "🌁",
	":bamboo:": "🎍",
	":ticket:": "🎫",
	":helicopter:": "🚁",
	":minidisc:": "💽",
	":oncoming_bus:": "🚍",
	":melon:": "🍈",
	":notebook:": "📓",
	":no_bell:": "🔕",
	":oden:": "🍢",
	":flags:": "🎏",
	":blowfish:": "🐡",
	":sweet_potato:": "🍠",
	":ski:": "🎿",
	":construction:": "🚧",
	":satellite:": "📡",
	":euro:": "💶",
	":ledger:": "📒",
	":leopard:": "🐆",
	":truck:": "🚚",
	":sake:": "🍶",
	":railway_car:": "🚃",
	":speedboat:": "🚤",
	":vhs:": "📼",
	":yen:": "💴",
	":mute:": "🔇",
	":wheelchair:": "♿",
	":paperclip:": "📎",
	":atm:": "🏧",
	":telescope:": "🔭",
	":rice_scene:": "🎑",
	":blue_book:": "📘",
	":postbox:": "📮",
	":e-mail:": "📧",
	":mouse2:": "🐁",
	":nut_and_bolt:": "🔩",
	":hotel:": "🏨",
	":wc:": "🚾",
	":green_book:": "📗",
	":tractor:": "🚜",
	":fountain:": "⛲",
	":metro:": "🚇",
	":clipboard:": "📋",
	":no_smoking:": "🚭",
	":slot_machine:": "🎰",
	":bathtub:": "🛁",
	":scroll:": "📜",
	":station:": "🚉",
	":rice_cracker:": "🍘",
	":bank:": "🏦",
	":wrench:": "🔧",
	":bar_chart:": "📊",
	":minibus:": "🚐",
	":tram:": "🚊",
	":microscope:": "🔬",
	":bookmark:": "🔖",
	":pouch:": "👝",
	":fax:": "📠",
	":sound:": "🔉",
	":chart:": "💹",
	":floppy_disk:": "💾",
	":post_office:": "🏣",
	":speaker:": "🔈",
	":japan:": "🗾",
	":mahjong:": "🀄",
	":orange_book:": "📙",
	":restroom:": "🚻",
	":train:": "🚋",
	":trolleybus:": "🚎",
	":postal_horn:": "📯",
	":factory:": "🏭",
	":train2:": "🚆",
	":pager:": "📟",
	":outbox_tray:": "📤",
	":mailbox:": "📫",
	":light_rail:": "🚈",
	":busstop:": "🚏",
	":file_folder:": "📁",
	":card_index:": "📇",
	":monorail:": "🚝",
	":no_bicycles:": "🚳",
	":hugging:": "🤗",
	":thinking:": "🤔",
	":nerd:": "🤓",
	":zipper_mouth:": "🤐",
	":rolling_eyes:": "🙄",
	":upside_down:": "🙃",
	":slight_smile:": "🙂",
	":writing_hand:": "✍",
	":eye:": "👁",
	":man_in_suit:": "🕴",
	":golfer:": "🏌",
	":golfer_woman:": "🏌‍♀",
	":anger_right:": "🗯",
	":coffin:": "⚰",
	":gear:": "⚙",
	":alembic:": "⚗",
	":scales:": "⚖",
	":keyboard:": "⌨",
	":shield:": "🛡",
	":bed:": "🛏",
	":ballot_box:": "🗳",
	":compression:": "🗜",
	":wastebasket:": "🗑",
	":file_cabinet:": "🗄",
	":trackball:": "🖲",
	":printer:": "🖨",
	":joystick:": "🕹",
	":hole:": "🕳",
	":candle:": "🕯",
	":prayer_beads:": "📿",
	":amphora:": "🏺",
	":label:": "🏷",
	":film_frames:": "🎞",
	":level_slider:": "🎚",
	":thermometer:": "🌡",
	":motorway:": "🛣",
	":synagogue:": "🕍",
	":mosque:": "🕌",
	":kaaba:": "🕋",
	":stadium:": "🏟",
	":desert:": "🏜",
	":cityscape:": "🏙",
	":camping:": "🏕",
	":rosette:": "🏵",
	":volleyball:": "🏐",
	":medal:": "🏅",
	":popcorn:": "🍿",
	":champagne:": "🍾",
	":hot_pepper:": "🌶",
	":burrito:": "🌯",
	":taco:": "🌮",
	":hotdog:": "🌭",
	":shamrock:": "☘",
	":comet:": "☄",
	":turkey:": "🦃",
	":scorpion:": "🦂",
	":lion_face:": "🦁",
	":crab:": "🦀",
	":spider_web:": "🕸",
	":spider:": "🕷",
	":chipmunk:": "🐿",
	":fog:": "🌫",
	":chains:": "⛓",
	":pick:": "⛏",
	":stopwatch:": "⏱",
	":ferry:": "⛴",
	":mountain:": "⛰",
	":ice_skate:": "⛸",
	":skier:": "⛷",
	":sad:": "😥",
	":egg:": "🥚",
	":drum:": "🥁"
};

function convertShortcodes(string) {
	if (string.split(":").length > 2) {
		for (var i in emojiShortCodes) {
			if (string.includes(i)) {
				string = string.replaceAll(i, emojiShortCodes[i]);
			}
		}
	}
	return string;
}

function escapeHtml(unsafe) {
    return unsafe
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

var sanitizeChat = function (string, maxlength = 500) {
	var temp = document.createElement("div");
	temp.innerText = string;
	temp.innerText = temp.innerHTML;
	temp = temp.textContent || temp.innerText || "";
	temp = temp.substring(0, Math.min(temp.length, maxlength));
	return temp.trim();
};

var sanitizeString = function (str) {
	str = str.replace(/[^a-z0-9áéíóúñü \.,_-]/gim, "");
	return str.trim();
};

var sanitizeLabel = function (string) {
	let temp = document.createElement("div");
	temp.innerText = string;
	temp.innerText = temp.innerHTML;
	temp = temp.textContent || temp.innerText || "";
	temp = temp.substring(0, Math.min(temp.length, 100));
	return temp.trim();
};

var sanitizeRoomName = function (roomid) {
	roomid = roomid.trim();
	if (roomid === "") {
		return roomid;
	} else if (roomid === false) {
		return roomid;
	}

	var sanitized = roomid.replace(/[\W]+/g, "_");
	if (roomid.replace(/ /g, "_") !== sanitized) {
		if (!session.cleanOutput) {
			warnUser("Info: Only AlphaNumeric characters should be used for the room name.\n\nThe offending characters have been replaced by an underscore");
		}
	}
	if (sanitized.length > 30) {
		sanitized = sanitized.substring(0, 30);
		if (!session.cleanOutput) {
			warnUser("The Room name should be less than 31 alPhaNuMeric characters long.\n\nWe will trim it to length.");
		}
	}
	return sanitized;
};

var sanitizePassword = function (passwrd) {
	if (passwrd === "") {
		return passwrd;
	} else if (passwrd === false) {
		return passwrd;
	} else if (passwrd === null) {
		return passwrd;
	}
	passwrd = passwrd.trim();
	if (passwrd.length < 1) {
		if (!session.cleanOutput) {
			warnUser("The password provided was blank.");
		}
	}
	var sanitized = encodeURIComponent(passwrd); //.replace(/[\W]+/g, "_");
	//if (sanitized !== passwrd) {
	//	if (!(session.cleanOutput)) {
	//		warnUser("Info: Only AlphaNumeric characters should be used in the password.\n\nThe offending characters have been replaced by an underscore");
	//	}
	//}
	return sanitized;
};

function checkConnection() {
	if (session.ws === null) {
		return;
	}
	if (!session.cleanOutput) {
		if (document.getElementById("qos")) {
			// true or false; null might cause problems?
			getById("logoname").style.display = "unset";
			if (session.ws && session.ws.readyState === WebSocket.OPEN) {
				getById("qos").style.color = "#FFF7";
			} else {
				getById("qos").style.color = "red";
			}
		}
	}
}

session.obsSceneSync = function () {
	if (session.layouts && session.obsSceneTriggers && session.obsState && session.obsState.details && session.obsState.details.currentScene.name && session.obsSceneTriggers.includes(session.obsState.details.currentScene.name)) {
		var idx = session.obsSceneTriggers.indexOf(session.obsState.details.currentScene.name);
		if (idx >= 0) {
			if (session.layouts[idx]) {
				var layout = combinedLayout(session.layouts[idx]);
				if (layout) {
					session.layout = layout;
					updateMixer();
				}
			}
		}
		return true;
	}
	return false;
};

session.sceneSync = function (UUID) {
	if (!session.rpcs[UUID]) {
		return;
	} else if (!session.rpcs[UUID].videoElement) {
		return;
	} // i'll want to consider other things, such as canvas at some point.

	var msg = {};
	msg.sceneDisplay = session.rpcs[UUID].videoElement.style.display != "none";
	msg.sceneMute = session.rpcs[UUID].mutedState;

	if (session.optimize !== false) {
		// if not visible in the scene anymore, lets lets optimize. This is outside the scope of OBS
		var bandwidth = parseInt(session.rpcs[UUID].targetBandwidth); // wtf is goign on here?
		if (msg.sceneDisplay === false) {
			if (bandwidth > session.optimize || bandwidth < 0) {
				// limit to optimized bitrate
				bandwidth = session.optimize;
			}
		}
		if (session.rpcs[UUID].bandwidth !== bandwidth) {
			// bandwidth already set correctly. don't resend.
			msg.bitrate = bandwidth;
			if (session.sendRequest(msg, UUID)) {
				session.rpcs[UUID].bandwidth = bandwidth; // this is letting the system know what the actual bandwidth is, even if it isn't the real target.
			} else {
				errorlog("Unable to set update OBS Visibility");
			}
		} else {
			session.sendRequest(msg, UUID);
		}
	} else {
		session.sendRequest(msg, UUID);
	}
};

var TriggerOnNewDetails = false;
session.obsStateSync = function (data2send = false, uid = false) {
	if (session.disableOBS) {
		return;
	}
	if (!window.obsstudio) {
		return;
	} // this isn't OBS
	// they can disable remote control via OBS brower source drop-down itself.

	log(data2send);

	if (data2send && data2send == "sourceActive" && session.obsState.sourceActive) {
		TriggerOnNewDetails = true;
	} else if (data2send && data2send == "details" && session.obsState.sourceActive && TriggerOnNewDetails) {
		if (session.obsState.details && session.obsState.details.currentScene && session.obsState.details.currentScene.name) {
			session.obsState.details.thisScene = session.obsState.details.currentScene.name;
			TriggerOnNewDetails = false;
		}
	}

	var needOptimize = false;
	if (session.obsState.visibility !== null) {
		if (session.obsState.visibility === false) {
			/////////////////// I need to change tis to .state or whatever, anc catch/handle these events to update the buttons in the pop up menu
			needOptimize = true;
		}
	}

	session.obsSceneSync();

	for (var UUID in session.rpcs) {
		if (uid && uid !== UUID) {
			continue;
		} // target just a single connection.

		var msg = {};
		if (!data2send) {
			msg.obsState = session.obsState;
			if (session.rpcs[UUID].obsControl === false) {
				msg.obsState.details = null; // we don't want to send needless data
			}
		} else if (data2send in session.obsState) {
			if (data2send == "details") {
				if (session.rpcs[UUID].obsControl === false) {
					continue; // we don't want to send needless data; this isn't a visibility update, so skip.
				}
				msg.obsState = {};
				msg.obsState[data2send] = session.obsState[data2send];
			} else {
				msg.obsState = {};
				msg.obsState[data2send] = session.obsState[data2send];
			}
		}

		if (session.filterOBSscenes && msg.obsState && msg.obsState.details && msg.obsState.details.scenes && msg.obsState.details.scenes.length) {
			var scenes = [];
			msg.obsState.details.scenes.forEach(scene => {
				if (session.filterOBSscenes && session.filterOBSscenes.length) {
					if (session.filterOBSscenes.includes(scene)) {
						scenes.push(scene);
					}
				}
			});
			msg.obsState.details.scenes = scenes;
		}

		if (session.optimize !== false) {
			var bandwidth = parseInt(session.rpcs[UUID].targetBandwidth);
			if (needOptimize) {
				if (bandwidth > session.optimize || bandwidth < 0) {
					// limit to optimized bitrate
					bandwidth = session.optimize;
				}
			}
			if (session.rpcs[UUID].bandwidth !== bandwidth) {
				// bandwidth already set correctly. don't resend.
				msg.bitrate = bandwidth;
				warnlog("Message to be sent: ");
				warnlog(msg);
				if (session.sendRequest(msg, UUID)) {
					session.rpcs[UUID].bandwidth = bandwidth; // this is letting the system know what the actual bandwidth is, even if it isn't the real target.
				} else {
					errorlog("Unable to set update OBS Visibility");
				}
			} else {
				warnlog("Message to be sent: ");
				warnlog(msg);
				session.sendRequest(msg, UUID);
			}
		} else {
			warnlog("Message to be sent: ");
			warnlog(msg);
			session.sendRequest(msg, UUID);
		}
	}
};

session.getOBSOptimization = function (msg, UUID) {
	if (session.obsState) {
		msg.obsState = {};
		var needOptimize = false;
		if (session.obsState.visibility !== null) {
			msg.obsState.visibility = session.obsState.visibility;
			if (session.obsState.visibility === false) {
				needOptimize = true;
			}
		}
		if (session.obsState.sourceActive !== null) {
			msg.obsState.sourceActive = session.obsState.sourceActive;
			//if (session.obsState.sourceActive===false){
			//	needOptimize=true;
			//}
		}
		if (session.obsState.recording !== null) {
			msg.obsState.recording = session.obsState.recording;
		}
		if (session.obsState.streaming !== null) {
			msg.obsState.streaming = session.obsState.streaming;
		}
		if (session.obsState.virtualcam !== null) {
			msg.obsState.virtualcam = session.obsState.virtualcam;
		}
	}
	if (session.optimize !== false) {
		msg.optimizedBitrate = parseInt(session.optimize) || 0; // not setting a bitrate; just letting them know what the optimized bitrate is.
		if (needOptimize) {
			session.rpcs[UUID].bandwidth = msg.optimizedBitrate;
		}
	}
	return msg;
};

function getOBSDetails(callbackname = "details") {
	if (session.disableOBS) {
		return false;
	}
	if (!window.obsstudio) {
		return;
	}

	if (!("details" in session.obsState)) {
		session.obsState.details = {};
	}

	var readOnlyFuncs = [
		"getControlLevel",
		//"getStatus",
		"getCurrentScene",
		"getScenes"
		//"getTransitions",
		//"getCurrentTransition",
		//"pluginVersion"
	];

	var promises = {};
	promises.main = true;

	Object.keys(window.obsstudio).forEach(async key => {
		try {
			if (typeof window.obsstudio[key] === "function") {
				if (readOnlyFuncs.includes(key)) {
					try {
						promises[key] = true;
						window.obsstudio[key](function (out) {
							var shortkey = key.replace("get", "");
							shortkey = shortkey[0].toLowerCase() + shortkey.slice(1);
							session.obsState.details[shortkey] = out;
							delete promises[key];
							if (!Object.keys(promises).length) {
								session.obsStateSync(callbackname);
							}
						});
					} catch (e) {
						delete promises[key];
					}
				}
				/* } else if (typeof window.obsstudio[key] === 'object'){ // none of these values I really need right now.
				var shortkey = key.replace("get","");
				shortkey = shortkey[0].toLowerCase() + shortkey.slice(1);
				session.obsState.details[shortkey] = window.obsstudio[key];
			} else {
				var shortkey = key.replace("get","");
				shortkey = shortkey[0].toLowerCase() + shortkey.slice(1);
				session.obsState.details[shortkey] = window.obsstudio[key]; */
			}
		} catch (e) {
			errorlog(e);
		}
	});
	delete promises.main;
	if (!Object.keys(promises).length) {
		session.obsStateSync(callbackname);
	}
}

function toggleOBSControls() {
	toggle(getById("remoteOBSControl"));
	if (getById("remoteOBSControl").style.display == "none") {
		getById("modalBackdrop").innerHTML = ""; // Delete modal
		getById("modalBackdrop").remove();
	} else {
		getById("modalBackdrop").innerHTML = ""; // Delete modal
		getById("modalBackdrop").remove();
		var modalTemplate = `<div id="modalBackdrop" style="z-index:24"></div>`;
		document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end
		document.getElementById("modalBackdrop").addEventListener("click", toggleOBSControls);
	}
}

function toggleOBSControlsLock(ele, disable = null) {
    const element = getById("remoteOBSControlContents");
    
    // If disable is not specified, toggle based on current state
    // Otherwise, use the provided value
    const shouldDisable = disable !== null ? disable : 
                         element.style.pointerEvents !== 'none';
    
    element.style.pointerEvents = shouldDisable ? 'none' : 'auto';
    element.style.opacity = shouldDisable ? '0.6' : '1';
    ele.textContent = shouldDisable ? '🔒' : '🔓';
	
    return shouldDisable; // returns the new state
}
function requestOBSAction(ele) {
	if (session.disableOBS) {
		return false;
	}
}
function obsSceneChanged(event) {
	log(event.detail.name);
	getOBSDetails(); // contains obsStateSync
}
function obsVirtualcamStarted(event) {
	session.obsState.virtualcam = true;
	session.obsStateSync("virtualcam");
}
function obsVirtualcamStopped(event) {
	session.obsState.virtualcam = false;
	session.obsStateSync("virtualcam");
}
function obsStreamingStarted(event) {
	session.obsState.streaming = true;
	session.obsStateSync("streaming");
}
function obsStreamingStopped(event) {
	session.obsState.streaming = false;
	session.obsStateSync("streaming");
}
function obsRecordingStarted(event) {
	session.obsState.recording = true;
	session.obsStateSync("recording");
}
function obsRecordingStopped(event) {
	session.obsState.recording = false;
	session.obsStateSync("recording");
}
function obsSourceActiveChanged(event) {
	warnlog("obsSourceActiveChanged");
	warnlog(event.detail);

	try {
		if (typeof event === "boolean") {
			var sourceActive = event;
		} else if (typeof event.detail === "boolean") {
			var sourceActive = event.detail;
		} else if (typeof event.detail.active === "boolean") {
			var sourceActive = event.detail.active;
		} else {
			var sourceActive = event.detail.active;
		}

		if (typeof sourceActive === "undefined") {
			return;
		} // Just fail.

		if (session.obsState.sourceActive !== sourceActive) {
			// only move forward if there is a change; the event likes to double fire you see.
			session.obsState.sourceActive = sourceActive;
			session.obsStateSync("sourceActive");
		}
	} catch (e) {
		errorlog(e);
	}
}

function obsSourceVisibleChanged(event) {
	// accounts for visible in VDO.Ninja scene AND visible in OBS scene
	warnlog("obsSourceVisibleChanged");
	warnlog(event.detail);
	try {
		if (typeof event === "boolean") {
			var visibility = event;
		} else if (typeof event.detail === "boolean") {
			var visibility = event.detail;
		} else if (typeof event.detail.visible === "boolean") {
			var visibility = event.detail.visible;
		} else {
			var visibility = event.detail.visible;
		}

		if (typeof visibility === "undefined") {
			// fall back
			if (typeof document.visibilityState !== "undefined") {
				visibility = document.visibilityState === "visible"; // modern
			} else if (typeof document.hidden !== "undefined") {
				visibility = !document.hidden; // legacy
			} else {
				return; // ... unknown input? fail.
			}
		}

		if (session.obsState.visibility !== visibility) {
			// only move forward if there is a change; the event likes to double fire you see.
			session.obsState.visibility = visibility;
			session.obsStateSync("visibility");
		}
	} catch (e) {
		errorlog(e);
	}
}

function manageSceneState(data, UUID) {
	// incoming obs details
	if (session.disableOBS) {
		return;
	}
	var processNeeded = false;
	try {
		if ("sceneDisplay" in data) {
			processNeeded = true;
			session.pcs[UUID].sceneDisplay = data.sceneDisplay;
		}
		if ("sceneMute" in data) {
			processNeeded = true;
			session.pcs[UUID].sceneMute = data.sceneMute;
		}

		if (data.obsState) {
			if ("sourceActive" in data.obsState) {
				processNeeded = true;
				session.pcs[UUID].obsState.sourceActive = data.obsState.sourceActive;
			}
			if ("visibility" in data.obsState) {
				processNeeded = true;
				session.pcs[UUID].obsState.visibility = data.obsState.visibility;
				session.optimizeBitrate(UUID); // &optimize flag; sets video bitrate to target value if this flag == HIDDEN (if optimize=0, disables both audio and video)
			}
			if ("details" in data.obsState) {
				//if (Object.keys(data.obsState.details).length){
					processNeeded = true;
					session.pcs[UUID].obsState.details = data.obsState.details;
				//}
			}
			if ("streaming" in data.obsState) {
				processNeeded = true;
				session.pcs[UUID].obsState.streaming = data.obsState.streaming;
			}
			if ("recording" in data.obsState) {
				processNeeded = true;
				session.pcs[UUID].obsState.recording = data.obsState.recording;
			}
			if ("virtualcam" in data.obsState) {
				processNeeded = true;
				session.pcs[UUID].obsState.virtualcam = data.obsState.virtualcam;
			}
		}
	} catch (e) {
		errorlog(e);
	}

	if (processNeeded) {
		log(data);
		applySceneState();
	} else {
		return;
	}

	if (isIFrame) {
		pokeIframeAPI("obs-state", data.obsState, UUID);
	}

	if (session.obsControls === false) {
		return;
	}

	try {
		var control = 0;
		if (session.pcs[UUID].obsState && session.pcs[UUID].obsState.details) {
			control = parseInt(session.pcs[UUID].obsState.details.controlLevel) || 0; //0 for NONE, 1 for READ_OBS (OBS data), 2 for READ_USER (User data), 3 for BASIC, 4 for ADVANCED and 5 for ALL
		}

		if (control >= 4) {
			if (session.director || !session.roomid) {
				if (session.pcs[UUID].remote) {
					if (session.obsControls !== false) {
						getById("obscontrolbutton").classList.remove("hidden"); // so they get a tip.
					}
				}
			}
		}

		var multi = false;
		getById("obsControlButtons")
			.querySelectorAll("[data-system]")
			.forEach(ele => {
				if (ele.dataset.system in session.pcs) {
					if (ele.dataset.system !== UUID) {
						multi = true;
					}
				} else {
					// delete, since no longer active.
					ele.remove();
				}
			});

		getById("obsSceneNames")
			.querySelectorAll("[data-system]")
			.forEach(ele => {
				if (ele.dataset.system in session.pcs) {
					if (ele.dataset.system !== UUID) {
						multi = true;
					}
				} else {
					// delete, since no longer active.
					ele.remove();
				}
			});

		if (control == 0) {
			var obsControlButtonsBox = getById("obsControlButtons").querySelector("[data-system='" + UUID + "']");
			if (obsControlButtonsBox) {
				obsControlButtonsBox.remove();
			}
			var obsSceneNamesBox = getById("obsSceneNames").querySelector("[data-system='" + UUID + "']"); // this hides if less than 2, so hide it now.
			if (obsSceneNamesBox) {
				obsSceneNamesBox.remove();
			}
			if (!multi) {
				getById("obsControlHelp").classList.remove("hidden");
			}
			return;
		}

		getById("obsControlHelp").classList.add("hidden");

		var obsControlButtonsBox = getById("obsControlButtons").querySelector("[data-system='" + UUID + "']");
		if (!obsControlButtonsBox) {
			obsControlButtonsBox = document.createElement("div");
			obsControlButtonsBox.dataset.system = UUID;
			getById("obsControlButtons").appendChild(obsControlButtonsBox);
		} else {
			obsControlButtonsBox.innerHTML = "";
		}

		if (multi) {
			var h3 = document.createElement("h3");
			h3.innerText = "OBS instance: " + (session.pcs[UUID].label || session.pcs[UUID].scene || UUID);
			obsControlButtonsBox.appendChild(h3);
		}

		if (session.pcs[UUID].obsState && "streaming" in session.pcs[UUID].obsState) {
			var controlButton = document.createElement("button");
			controlButton.dataset.UUID = UUID;

			if (session.pcs[UUID].obsState.streaming) {
				controlButton.classList.add("pressed");
				controlButton.ariaPressed = "true";
				controlButton.dataset.obsAction = "stopStreaming";
				controlButton.innerText = "📡 stop streaming";
				controlButton.classList.remove("hidden");
			} else if (session.pcs[UUID].obsState.streaming===false) {
				controlButton.classList.remove("hidden");
				controlButton.dataset.obsAction = "startStreaming";
				controlButton.innerText = "📡 start streaming";
			} else {
				controlButton.dataset.obsAction = "startStreaming";
				controlButton.innerText = "📡 start streaming";
				controlButton.classList.remove("hidden");
			}

			if (control < 5) {
				controlButton.disabled = true;
				controlButton.style.cursor = "not-allowed";
				controlButton.title = "Source is lacking required permissions.";
			} else {
				controlButton.onclick = async function () {
					var msg = {};
					msg.obsCommand = {};
					msg.obsCommand.action = this.dataset.obsAction;
					msg.UUID = this.dataset.UUID;
					if (document.querySelector("#obsRemotePassword>input") && document.querySelector("#obsRemotePassword>input").value) {
						msg.remote = document.querySelector("#obsRemotePassword>input").value;
					} else {
						msg.remote = session.remote;
					}
					msg = await session.encodeRemote(msg);
					session.anysend(msg); // this is neat, but doesn't work with websocket. I need to add
					log("action request: " + this.dataset.obsAction);
				};
			}
			obsControlButtonsBox.appendChild(controlButton);
		}
		if (session.pcs[UUID].obsState && "recording" in session.pcs[UUID].obsState) {
			var controlButton = document.createElement("button");
			controlButton.dataset.UUID = UUID;

			if (session.pcs[UUID].obsState.recording) {
				controlButton.classList.add("pressed");
				controlButton.ariaPressed = "true";
				controlButton.dataset.obsAction = "stopRecording";
				controlButton.innerText = "📽 stop recording";
				controlButton.classList.remove("hidden");
			} else if (session.pcs[UUID].obsState.recording===false) {
				controlButton.classList.remove("hidden");
				controlButton.dataset.obsAction = "startRecording";
				controlButton.innerText = "📽 start recording";
			} else {
				controlButton.classList.remove("hidden");
				controlButton.dataset.obsAction = "startRecording";
				controlButton.innerText = "📽 start recording";
			}

			if (control < 5) {
				controlButton.disabled = true;
				controlButton.style.cursor = "not-allowed";
				controlButton.title = "Source is lacking required permissions.";
			} else {
				controlButton.onclick = async function () {
					var msg = {};
					msg.obsCommand = {};
					msg.obsCommand.action = this.dataset.obsAction;
					msg.UUID = this.dataset.UUID;
					if (document.querySelector("#obsRemotePassword>input").value) {
						msg.remote = document.querySelector("#obsRemotePassword>input").value;
					} else {
						msg.remote = session.remote;
					}
					msg = await session.encodeRemote(msg);
					session.anysend(msg);
					log("action request: " + this.dataset.obsAction);
				};
			}
			obsControlButtonsBox.appendChild(controlButton);
		}
		if (session.pcs[UUID].obsState && "virtualcam" in session.pcs[UUID].obsState) {
			var controlButton = document.createElement("button");

			controlButton.dataset.UUID = UUID;

			if (session.pcs[UUID].obsState.virtualcam) {
				controlButton.classList.add("pressed");
				controlButton.ariaPressed = "true";
				controlButton.dataset.obsAction = "stopVirtualcam";
				controlButton.innerText = "💻 stop virtualcam";
				controlButton.classList.remove("hidden");
			} else if (session.pcs[UUID].obsState.virtualcam===false) {
				controlButton.classList.remove("hidden");
				controlButton.dataset.obsAction = "startVirtualcam";
				controlButton.innerText = "💻 start virtualcam";
			} else {
				controlButton.classList.remove("hidden");
				controlButton.dataset.obsAction = "startVirtualcam";
				controlButton.innerText = "💻 start virtualcam";
			}

			if (control < 5) {
				controlButton.disabled = true;
				controlButton.style.cursor = "not-allowed";
				controlButton.title = "Source is lacking required permissions.";
			} else {
				controlButton.onclick = async function () {
					var msg = {};
					msg.obsCommand = {};
					msg.obsCommand.action = this.dataset.obsAction;
					msg.UUID = this.dataset.UUID;
					if (document.querySelector("#obsRemotePassword>input").value) {
						msg.remote = document.querySelector("#obsRemotePassword>input").value;
					} else {
						msg.remote = session.remote;
					}
					msg = await session.encodeRemote(msg);
					session.anysend(msg);
					log("action request: " + this.dataset.obsAction);
				};
			}
			obsControlButtonsBox.appendChild(controlButton);
		}
	} catch (e) {
		errorlog(e);
	} // just in case the client has disconnected.

	if (control < 2) {
		var obsSceneNamesBox = getById("obsSceneNames").querySelector("[data-system='" + UUID + "']");
		if (obsSceneNamesBox) {
			obsSceneNamesBox.remove();
		}
		return;
	}

	var obsSceneNamesBox = getById("obsSceneNames").querySelectorAll("div[data-system='" + UUID + "']");
	if (!obsSceneNamesBox.length) {
		obsSceneNamesBox = document.createElement("div");
		obsSceneNamesBox.dataset.system = UUID;
		getById("obsSceneNames").appendChild(obsSceneNamesBox);
	} else {
		obsSceneNamesBox = obsSceneNamesBox[0];
		obsSceneNamesBox.innerHTML = "";
	}

	if (multi) {
		var h3 = document.createElement("h3");
		h3.innerText = "OBS instance: " + (session.pcs[UUID].label || session.pcs[UUID].scene || UUID);
		obsSceneNamesBox.appendChild(h3);
	}

	if (session.pcs[UUID].obsState.details) {
		var details = session.pcs[UUID].obsState.details;
		if (details.scenes) {
			details.scenes.forEach(scene => {
				var sceneButton = document.createElement("button");
				sceneButton.dataset.obsScene = scene;
				sceneButton.dataset.UUID = UUID;
				sceneButton.innerText = scene;
				if (details.currentScene && details.currentScene.name && details.currentScene.name === scene) {
					sceneButton.classList.add("pressed");
					sceneButton.ariaPressed = "true";
				}
				obsSceneNamesBox.appendChild(sceneButton);
				if (control < 4) {
					sceneButton.disabled = true;
					sceneButton.style.cursor = "not-allowed";
					sceneButton.title = "Source is lacking required permissions.";
				} else {
					sceneButton.onclick = async function () {
						var msg = {};
						msg.obsCommand = { action: "setCurrentScene", value: this.dataset.obsScene };
						msg.UUID = this.dataset.UUID;
						if (document.querySelector("#obsRemotePassword>input").value) {
							msg.remote = document.querySelector("#obsRemotePassword>input").value;
						} else {
							msg.remote = session.remote;
						}
						msg = await session.encodeRemote(msg);
						session.anysend(msg);
						log("scene change request: " + this.dataset.obsScene);
					};
				}
			});
		}
	}
	getById("debugRemoteOBSControl").innerText = JSON.stringify(session.pcs[UUID].obsState);
}

function processOBSCommand(msg) {
	if (session.disableOBS) {
		return false;
	} else if (!window.obsstudio) {
		return false;
	} else if (typeof msg.obsCommand !== "object") {
		return false;
	} else if ("remote" in msg) {
		if ((msg.remote === session.remote && session.remote) || session.remote === true) {
			// approved
		} else {
			if (msg.UUID && msg.obsCommand.action) {
				var data = {};
				data.rejected = "obsCommand";
				//data.debug = msg.remote;
				session.sendRequest(data, msg.UUID); // this skips the server
			}
			warnlog("Denied access; remote does not match");
			return false;
		}
	} else {
		if (msg.UUID && msg.obsCommand.action) {
			var data = {};
			data.rejected = "obsCommand";
			//data.debug = "no remote code provided";
			session.sendRequest(data, msg.UUID); // this skips the server
		}
		return false;
	}
	
	try {
		//  {changeScene: this.dataset.obsScene}
		if (msg.obsCommand.action && typeof msg.obsCommand.action == "string") {
			
			if (msg.obsCommand.action=="stopVirtualcam" || msg.obsCommand.action=="startVirtualcam"){
				if (session.obsState.virtualcam===false){
					if (msg.UUID){
						var data = {};
						data.rejected = msg.obsCommand.action;
						session.sendRequest(data, msg.UUID); // this skips the server
					}
					return false;
				}
			}
			if (msg.obsCommand.action=="stopRecording" || msg.obsCommand.action=="startRecording"){
				if (session.obsState.recording===false){
					if (msg.UUID){
						var data = {};
						data.rejected = msg.obsCommand.action;
						session.sendRequest(data, msg.UUID); // this skips the server
					}
					return false;
				}
			}
			if (msg.obsCommand.action=="stopStreaming" || msg.obsCommand.action=="startStreaming"){
				if (session.obsState.streaming===false){
					if (msg.UUID){
						var data = {};
						data.rejected = msg.obsCommand.action;
						session.sendRequest(data, msg.UUID); // this skips the server
					}
					return false;
				}
			}
			
			if (msg.obsCommand.value && typeof msg.obsCommand.value == "string") {
				if (msg.obsCommand.action == "setCurrentScene" && session.filterOBSscenes && session.filterOBSscenes.length) {
					try {
						if (!session.filterOBSscenes.includes(msg.obsCommand.value)) {
							return false;
						}
					} catch (e) {
						errorlog(e);
						return false;
					}
				}
				window.obsstudio[msg.obsCommand.action](msg.obsCommand.value);
			} else {
				window.obsstudio[msg.obsCommand.action]();
			}
		}
	} catch (e) {
		errorlog(e);
		return false;
	}
	return true;
}

function applySceneState() {
	// guest side; tally light, etc.

	if (document.getElementById("videosource")) {
		var visibility = false;
		var ondeck = false;
		var recording = false;

		if (session.tallyOverride !== false) {
			if (session.tallyOverride == 1) {
				recording = true;
			} else if (session.tallyOverride == 2) {
				ondeck = true;
				visibility = false;
				recording = false;
			} else if (session.tallyOverride == 3) {
				visibility = true;
				recording = false;
			} else if (session.tallyOverride == 0) {
				ondeck = false;
				visibility = false;
				recording = false;
			} else {
				// maybe its a custom message or default?
			}
			if (!session.cleanOutput) {
				getById("obsState").classList.remove("hidden");
			}
		} else if (!session.disableOBS) {
			for (var uid in session.pcs) {
				if (session.pcs[uid].obsState.sourceActive !== false && session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) {
					visibility = true;
				} else if (session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) {
					ondeck = true;
				}
				if ((session.pcs[uid].obsState.recording || session.pcs[uid].obsState.streaming) && session.pcs[uid].obsState.sourceActive !== false && session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) {
					// the scene that is recording must be visible also.
					recording = true;
				}
			}
			if (!session.cleanOutput) {
				getById("obsState").classList.remove("hidden");
			}
		} else {
			return;
		}

		if (recording) {
			getById("obsState").classList.remove("ondeck");
			getById("obsState").classList.add("recording"); // TODO: this needs to check all peers to make sure it's valid
			getById("obsState").innerHTML = "ON AIR";

			if (session.tallyStyle) {
				getById("main").classList.remove("ondeck");
				getById("main").classList.add("recording");
			}
		} else if (visibility) {
			getById("obsState").classList.remove("recording");
			getById("obsState").classList.remove("ondeck");
			getById("obsState").innerHTML = "ACTIVE";

			if (session.tallyStyle) {
				// only show active if its tally is enabled manually
				getById("main").classList.remove("recording");
				getById("main").classList.remove("ondeck");
			} else {
				getById("obsState").classList.add("hidden"); // most people don't care about being active
			}
		} else if (ondeck) {
			getById("obsState").classList.remove("recording");
			getById("obsState").classList.add("ondeck"); // TODO: this needs to check all peers to make sure it's valid
			getById("obsState").innerHTML = "STAND BY";

			if (session.tallyStyle) {
				getById("main").classList.remove("recording");
				getById("main").classList.add("ondeck");
			}
		} else {
			getById("obsState").classList.remove("recording");
			getById("obsState").classList.remove("ondeck");
			getById("obsState").innerHTML = "INACTIVE";
			getById("obsState").classList.add("hidden"); // I don't think most people care to see inactive.

			if (session.tallyStyle) {
				getById("main").classList.remove("recording");
				getById("main").classList.remove("ondeck");
			}
		}

		//miniTranslate(getById("obsState"));

		if (visibility) {
			// BASIC TALLY LIGHT (on deck disabled)
			getById("obsState").classList.add("onair"); // LIVE
			if (session.tallyStyle) {
				getById("main").classList.add("onair");
			}
		} else {
			getById("obsState").classList.remove("onair");
			if (session.tallyStyle) {
				getById("main").classList.remove("onair");
			}
		}

		if (session.automute) {
			if (!visibility) {
				session.micIsolatedAutoMute = [];
				if (session.automute !== "2") {
					for (var uid in session.pcs) {
						if (session.directorList.indexOf(uid) >= 0) {
							// allow validated directors to hear the guest
							session.micIsolatedAutoMute.push(uid);
						}
					}
				}
			} else {
				session.micIsolatedAutoMute = false;
			}
			session.applyIsolatedChat();
		}
	}
}

function compare_vids(a, b) {
	var aa = a.order || 0;
	var bb = b.order || 0;
	if (aa < bb) {
		return 1;
	}
	if (aa > bb) {
		return -1;
	}
	return 0;
}

function compare_vids_sid(a, b) {
	var aa = a.dataset.sid || 0;
	var bb = b.dataset.sid || 0;
	if (aa > bb) {
		return 1;
	}
	if (aa < bb) {
		return -1;
	}
	return 0;
}
function compare_vids_label(a, b) {
	if (a.dataset.UUID && session.rpcs[a.dataset.UUID] && session.rpcs[a.dataset.UUID].label) {
		var aa = session.rpcs[a.dataset.UUID].label.toLowerCase();
	} else {
		var aa = 0;
	}

	if (b.dataset.UUID && session.rpcs[b.dataset.UUID] && session.rpcs[b.dataset.UUID].label) {
		var bb = session.rpcs[b.dataset.UUID].label.toLowerCase();
	} else {
		var bb = 0;
	}

	if (aa > bb) {
		return 1;
	}
	if (aa < bb) {
		return -1;
	}
	return 0;
}

function sortByZ(mediaPool, layout) {
	function sortABZ(a, b) {
		if (layout[a.dataset.sid]) {
			var aa = layout[a.dataset.sid].zIndex || layout[a.dataset.sid].z || 0;
		} else {
			var aa = 0;
		}
		if (layout[b.dataset.sid]) {
			var bb = layout[b.dataset.sid].zIndex || layout[b.dataset.sid].z || 0;
		} else {
			var bb = 0;
		}
		if (aa < bb) {
			return -1;
		}
		if (aa > bb) {
			return 1;
		}
		return 0;
	}
	mediaPool.sort(sortABZ);
	return mediaPool;
}

window.onpopstate = function () {
	if (session.firstPlayTriggered) {
		window.location.reload(true); // deprecated, but it seems to work, so w/e
	}
};

var miniPerformerX = null;
var miniPerformerY = null;
function makeMiniDraggableElement(elmnt) {
	if (session.disableMouseEvents) {
		return;
	}

	try {
		elmnt.dragElement = false;
		// elmnt.style.bottom = "auto";
		elmnt.style.cursor = "grab";

		elmnt.stashonmouseup = null;
		elmnt.stashonmousemove = null;
	} catch (e) {
		errorlog(e);
		return;
	}

	var pos1 = 0;
	var pos2 = 0;
	var pos3 = 0;
	var pos4 = 0;

	var timestamp = false;

	function elementDrag(e) {
		// ON DRAG
		timestamp = false;
		if (session.infocus) {
			return;
		}
		try {
			e = e || window.event;

			if (e.type !== "touchmove") {
				if ("buttons" in e && e.buttons !== 1) {
					closeDragElement(e);
					return;
				}
				e.preventDefault();
			}
			e.stopPropagation();

			elmnt.dragElement = true;

			if (e.type === "touchmove") {
				pos1 = pos3 - e.touches[0].clientX;
				pos2 = pos4 - e.touches[0].clientY;
				pos3 = e.touches[0].clientX;
				pos4 = e.touches[0].clientY;
			} else {
				pos1 = pos3 - e.clientX;
				pos2 = pos4 - e.clientY;
				pos3 = e.clientX;
				pos4 = e.clientY;
			}

			var topDrag = elmnt.offsetTop - pos2;
			if (topDrag > -3 + (window.innerHeight - elmnt.clientHeight)) {
				topDrag = -3 + (window.innerHeight - elmnt.clientHeight);
			}

			miniPerformerY = topDrag;
			miniPerformerX = elmnt.offsetLeft - pos1;

			if (miniPerformerY > window.innerHeight - elmnt.clientHeight) {
				miniPerformerY = window.innerHeight - elmnt.clientHeight;
			}
			if (miniPerformerX > window.innerWidth - elmnt.clientWidth) {
				miniPerformerX = window.innerWidth - elmnt.clientWidth;
			}

			miniPerformerX = (100 * miniPerformerX) / window.innerWidth;
			miniPerformerY = (100 * miniPerformerY) / window.innerHeight;

			if (session.widget && !session.leftMiniPreview) {
				if (miniPerformerX > 74) {
					miniPerformerX = 74;
				}
			}

			if (miniPerformerY < 0) {
				miniPerformerY = 0;
			} else if (miniPerformerY > 100) {
				miniPerformerY = 100;
			}
			if (miniPerformerX < 0) {
				miniPerformerX = 0;
			} else if (miniPerformerX > 100) {
				miniPerformerX = 100;
			}

			elmnt.style.right = "unset";
			elmnt.style.top = miniPerformerY + "%";
			elmnt.style.left = miniPerformerX + "%";
		} catch (e) {
			errorlog(e);
		}
	}

	function closeDragElement(e) {
		// TOUCH END
		e = e || window.event;

		if (e.type !== "touchend") {
			if (e.button !== 0) {
				return;
			}
			document.onmouseup = elmnt.stashonmouseup;
			document.onmousemove = elmnt.stashonmousemove;
			elmnt.onmouseleave = null;
		}

		if (session.infocus) {
			return;
		}
		e.preventDefault();

		if (timestamp && Date.now() - timestamp > 500) {
			// long hold, so this is a drag
			e.stopPropagation();
			if (e.type === "touchend") {
				if (session.infocus === true) {
					session.infocus = false;
				} else {
					session.infocus = true;
					log("session: myself");
				}
				setTimeout(() => updateMixer(), 10);
			}
		} else if (timestamp && e.type !== "touchend") {
			if (session.infocus === true) {
				session.infocus = false;
			} else {
				session.infocus = true;
				log("session: myself");
			}
			setTimeout(() => updateMixer(), 10);
		}
	}

	function dragMouseDown(e) {
		////// TOUCH START

		if (event.ctrlKey || event.metaKey) {
			return;
		}

		timestamp = Date.now();

		e = e || window.event;
		if (session.infocus) {
			return;
		}

		e.preventDefault();
		if (e.type === "touchstart") {
			pos3 = e.touches[0].clientX;
			pos4 = e.touches[0].clientY;

			elmnt.ontouchend = closeDragElement;
			elmnt.ontouchmove = elementDrag;
		} else {
			if (e.button !== 0) {
				return;
			}
			pos3 = e.clientX;
			pos4 = e.clientY;
			elmnt.stashonmouseup = document.onmouseup; // I don't want to interfere with other drag events.
			elmnt.stashonmousemove = document.onmousemove;

			document.onmouseup = closeDragElement;
			document.onmousemove = elementDrag;
			elmnt.onmouseleave = function (event) {
				closeDragElement(event);
			};
		}
	}

	elmnt.onmousedown = dragMouseDown;
	elmnt.ontouchstart = dragMouseDown;
}

function makeDraggableElement(element) {
	if (session.disableMouseEvents) {
		return;
	} // this is here for a reason. :P
	if (!element){
		return;
	}
	element.initialX;
	element.initialY;
	element.currentX;
	element.xOffset = 0;
	element.currentY;
	element.yOffset = 0;
	element.isDragging = false;
	element.dragElement = true;

	element.addEventListener("mousedown", dragStart);

	function dragStart(e) {
		element.initialX = e.clientX - element.xOffset;
		element.initialY = e.clientY - element.yOffset;

		document.addEventListener("mousemove", drag);
		document.addEventListener("mouseup", dragEnd);
		document.addEventListener("onmouseleave", dragEnd);
		document.addEventListener("onmouseenter", dragEnd);

		element.isDragging = true;
	}

	function dragEnd(e) {
		element.initialX = element.currentX;
		element.initialY = element.currentY;

		document.removeEventListener("mousemove", drag);
		document.removeEventListener("mouseup", dragEnd);
		document.removeEventListener("onmouseleave", dragEnd);
		document.removeEventListener("onmouseenter", dragEnd);

		element.isDragging = false;
	}

	function drag(e) {
		if (element.isDragging) {
			element.currentX = e.clientX - element.initialX;
			element.currentY = e.clientY - element.initialY;

			// Get the dimensions of the viewport
			let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
			let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);

			// Get the dimensions of the object
			let elementWidth = element.offsetWidth;
			let elementHeight = element.offsetHeight;
			// console.log('elementWidth:\n',elementWidth)
			// console.log('elementHeight:\n',elementHeight)

			// Calculate the boundaries
			let maxX = vw - elementWidth;
			let maxY = vh - elementHeight;
			let minX = 0;
			let minY = 0;

			// Calculate real boundaries (parent position: fixed issues)
			let topOffset = 0;
			let leftOffset = 0;
			let elementOffset = element;
			while (elementOffset) {
				topOffset += elementOffset.offsetTop;
				leftOffset += elementOffset.offsetLeft;
				elementOffset = elementOffset.offsetParent;
			}

			// Adjust the position if it's going beyond the boundaries
			let realX = element.currentX + leftOffset;
			let realY = element.currentY + topOffset;

			if (realX > maxX) {
				element.currentX = maxX - leftOffset;
			} else if (realX < minX) {
				element.currentX = minX - leftOffset;
			}

			if (realY > maxY) {
				element.currentY = maxY - topOffset;
			} else if (realY < minY) {
				element.currentY = minY - topOffset;
			}
			// Update the position and offset
			element.xOffset = element.currentX;
			element.yOffset = element.currentY;

			element.style.transform = `translate(${element.currentX}px, ${element.currentY}px)`;
		}
	}
}

function clearCacheForCurrentSite() {
  if ('caches' in window) {
	  try {
		caches.keys().then(function(cacheNames) {
		  cacheNames.forEach(function(cacheName) {
			caches.delete(cacheName);
		  });
		});
		 log("Cache cleared for current site");
	  } catch(e){}
  } else {
    warnlog("Cache API not supported");
  }
}

function removeStorage(cname) {
	localStorage.removeItem(cname);
}

function clearStorage() {
	localStorage.clear();
	//clearCacheForCurrentSite(); // cache as well.
	if (!session.cleanOutput) {
		warnUser("The local storage and saved settings have been cleared", 1000);
	}
}

function setStorage(cname, cvalue, hours = 9999) {
	// not actually a cookie
	var now = new Date();
	var item = {
		value: cvalue,
		expiry: now.getTime() + hours * 60 * 60 * 1000
	};
	try {
		localStorage.setItem(cname, JSON.stringify(item));
	} catch (e) {
		errorlog(e);
	}
}

function getStorage(cname) {
	try {
		var itemStr = localStorage.getItem(cname);
	} catch (e) {
		errorlog(e);
		return;
	}
	if (!itemStr) {
		return "";
	}
	var item = JSON.parse(itemStr);
	var now = new Date();
	if (now.getTime() > item.expiry) {
		localStorage.removeItem(cname);
		return "";
	}
	return item.value;
}

function play(streamid = null, UUID = false) {
	// play whatever is in the URL params; or filter by a streamID option
	log("play stream: " + session.view + " " + streamid);

	if (session.viewDirectorOnly) {
		if (!(UUID || streamid)) {
			warnlog("No UUID and StreamID");
			return;
		} else if (session.directorList.indexOf(UUID) == -1) {
			warnlog("Not a director");
			return;
		}
	}

	if (session.view_set) {
		var played = false;
		for (var j in session.view_set) {
			if (streamid === null) {
				// play what is in the view list ; not a group room probably
				session.watchStream(session.view_set[j]);
				played = true;
			} else if (streamid === session.view_set[j]) {
				// plays if the group room list matches the explicit list
				session.watchStream(session.view_set[j]);
				played = true;
			}
		}
		if (session.include) {
			session.include.forEach(sid => {
				if (session.view_set.includes(sid)) {
					// already played
				} else if (streamid === null) {
					// play what is in the view list ; not a group room probably
					session.watchStream(sid);
				} else if (streamid === sid) {
					// plays if the group room list matches the explicit list
					session.watchStream(sid);
					played = true;
				}
			});
		}

		if (!played && streamid) {
			if (session.scene !== false) {
				if (!session.permaid) {
					if (!session.queue) {
						// I don't want to deal with queues.
						if (session.exclude === false || !session.exclude.includes(streamid)) {
							if (UUID) {
								if (session.directorList.indexOf(UUID) >= 0) {
									warnlog("stream ID added to badStreamList: " + streamid);
									session.badStreamList.push(streamid);
									// if I uncomment this, the director can mute the solo link.
									// session.watchStream(streamid); // changed June 4th 2024. We shouldn't be viewing the stream if on the bad list, no?
								}
							}
						}
					}
				}
			}
		}
	} else if (streamid && session.exclude !== false) {
		if (session.exclude.includes(streamid)) {
			// we don't play it at all. (if explicity listed as VIDEO, then OKay.)
		} else {
			session.watchStream(streamid); // I suppose we do play it.
		}
	} else if (streamid) {
		if (session.optimize === 0){
			log("Running special optimize===logic logic for loading rtc connections");
			try {
				// must be a scene and not an auto scene
				if (session.scene && session.activatedStreams.size){
					if (session.activatedStreams.has(streamid)){
						session.watchStream(streamid);
						return;
					}
				}
				if (UUID && streamid) {
					if (session.directorList.indexOf(UUID) >= 0) {
						session.watchStream(streamid);
					}
				}
			} catch(e){
				errorlog(e);
			}
		} else {
			session.watchStream(streamid);
		}
	} else if (session.include.length) {
		session.include.forEach(sid => {
			session.watchStream(sid);
		});
	}
}

function nextQueue() {
	if (!session.queue) {
		return;
	}
	if (!session.director) {
		return;
	}
	if (session.queueList.length == 0) {
		getById("queuebutton").classList.add("red");
		setTimeout(function () {
			getById("queuebutton").classList.remove("red");
		}, 50);
		return;
	}
	var nextStream = session.queueList.shift();

	getById("queuebutton").classList.add("red");
	setTimeout(function () {
		getById("queuebutton").classList.remove("red");
	}, 200);

	updateQueue();

	session.watchStream(nextStream);
	log("next stream loading: " + nextStream);
}

function updateQueue(adding = false) {
	if (!session.queue) {
		return;
	}
	if (!session.director) {
		return;
	}
	if (session.queueList.length) {
		if (session.queueList.length > 10) {
			getById("queueNotification").innerHTML = "‼";
		} else {
			getById("queueNotification").innerHTML = session.queueList.length;
		}
		getById("queueNotification").classList.add("queueNotification");
	} else {
		getById("queueNotification").innerHTML = "";
		getById("queueNotification").classList.remove("queueNotification");
	}

	if (adding) {
		if (session.beepToNotify) {
			playtone();
			showNotification("someone joined the queue", "queue length: " + session.queueList.length);
		}
		getById("queuebutton").classList.remove("shake");
		setTimeout(function () {
			getById("queuebutton").classList.add("shake");
		}, 10);
	}
}

function hideStreamLowBandwidth(bandwidth, UUID) {
	if (session.lowBitrateCutoff===false) { // allow 0,but I should probably also do this if there is a disconnect.
		return;
	}

	if (session.directorList.includes(UUID) || session.rpcs[UUID].director) {
		if (session.showDirector || session.rpcs[UUID].showDirector) {
			// all good
		} else {
			return; // we don't include the director since not treated as a guest
		}
	}

	if (bandwidth <= session.lowBitrateCutoff) {
		//log("bandwidth <= session.lowBitrateCutoff");
		log("actual bandwidth: "+ bandwidth+ " < bandwidth cut threshold: " +session.lowBitrateCutoff);
		// <= used so 0 can be used as a trigger
		if (session.lowBitrateSceneChange) {
			changeSceneLowBandwidth(true);
		} else if (!session.rpcs[UUID].bandwidthMuted) {
			session.rpcs[UUID].bandwidthMuted = true;
			updateMixer();
		}
	} else if (session.lowBitrateSceneChange) {
		log("changeSceneLowBandwidth(false)");
		changeSceneLowBandwidth(false);
	} else if (session.rpcs[UUID].bandwidthMuted) {
		session.rpcs[UUID].bandwidthMuted = false;
		if (session.rpcs[UUID].videoElement) {
			session.rpcs[UUID].videoElement.muted = checkMuteState(UUID);
		}
		updateMixer();
	}
}

var changeSceneEnabled = false;
var changeSceneLowBandwidthRevert = false;
function changeSceneLowBandwidth(state) {
	if (!session.lowBitrateSceneChange) {
		return;
	}
	if (!session.obsState) {
		return;
	}
	try {
		if (session.obsState.sourceActive && session.obsState.details && session.obsState.details.currentScene) {
			
			changeSceneLowBandwidthRevert = session.obsState.details.currentScene.name || false;
		} else if ("sourceActive" in session.obsState && !session.obsState.sourceActive && session.obsState.details && session.obsState.details.currentScene) {
			if (session.obsState.details.currentScene.name !== session.lowBitrateSceneChange) {
				return; // not the FML scene, nor are we visible, so we're not going to switch back.  Assume the user has overtaken the setup.
			}
		}
		if (!window.obsstudio || !window.obsstudio["setCurrentScene"]) {
			return;
		}
		if (state && changeSceneLowBandwidthRevert) {
			if (changeSceneEnabled) {
				// bitrate was higher , so we can now cut off.
				//log("2 changeSceneEnabled: "+session.lowBitrateSceneChange+" changeSceneEnabled:"+changeSceneEnabled);
				log("Changing to cut scene due to low bandwidth");
				window.obsstudio["setCurrentScene"](session.lowBitrateSceneChange);
			} else {
				log("Low bandwidth, but not changing scenes because threshold not hit at least once yet");
			}
		} else if (changeSceneLowBandwidthRevert) { 
			changeSceneEnabled = true;
			//log("1 setCurrentScene: "+changeSceneLowBandwidthRevert+" changeSceneEnabled:"+changeSceneEnabled);
			log("Reverting to original scene due to good bandwidth");
			window.obsstudio["setCurrentScene"](changeSceneLowBandwidthRevert);
		}
	} catch (e) {
		errorlog(e);
	}
}

function setupIncomingScreenTracking(v, UUID) {
	// SCREEN  element.

	if (session.directorList.indexOf(UUID) >= 0) {
		v.muted = false;
	}

	v.addEventListener(
		"playing",
		e => {
			try {
				var bigPlayButton = document.getElementById("bigPlayButton");
				if (bigPlayButton) {
					bigPlayButton.parentNode.removeChild(bigPlayButton);
				}
			} catch (e) {}

			resetupAudioOut(e.target, true);

			try {
				if (session.pip) {
					if (v.readyState >= 3) {
						if (!v.pip) {
							v.pip = true;
							toggleSystemPip(v, true);
						}
					}
				}
			} catch (e) {}
		},
		{ once: true }
	);

	v.onpause = event => {
		// prevent things from pausing; human or other

		if (v.dataset.UUID && session.rpcs[v.dataset.UUID] && session.rpcs[v.dataset.UUID].manualBandwidth === 0) {
			return true;
		}
		if (!(event.ctrlKey || event.metaKey)) {
			warnlog("Video paused; force it to play again");
			//return;
			//session.audioCtx.resume();
			//log("ctx resume");

			event.currentTarget
				.play()
				.then(_ => {
					log("playing 4");
				})
				.catch(error => {
					warnlog("didnt play 1");
				});
			if (Firefox) {
				unPauseVideo(v);
			}
		}
		return true;
	};

	if (session.pip) {
		v.onloadedmetadata = function () {
			if (!v.paused) {
				if (!v.pip) {
					v.pip = true;
					toggleSystemPip(v, true);
				}
			}
		};
	}

	v.addEventListener("resize", e => {
		// if the aspect ratio changes, then we might want to update the mixer.  If audio only, then this doesn't matter.
		var v = e.target;
		var aspectRatio = parseFloat(v.videoWidth / v.videoHeight) || 0;
		log("resize event: " + aspectRatio);

		if (!aspectRatio) {
			v.resetAR = true;
			return;
		} // if Audio only, then we don't want to set or update any aspect ratio.

		if (v.resetAR) {
			log("ASPECT RATIO UNMUTED");
			delete v.resetAR;
			v.dataset.aspectRatio = aspectRatio;
			pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
			setTimeout(function () {
				updateMixer();
			}, 1);
		} else if (v.dataset.aspectRatio) {
			if (aspectRatio != parseFloat(v.dataset.aspectRatio)) {
				log("ASPECT RATIO CHANGED");
				v.dataset.aspectRatio = aspectRatio;
				pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
				setTimeout(function () {
					updateMixer();
				}, 1); // We don't want to run this on the first resize?  just subsequent ones.
			}
		} else {
			log("NEW VIDEO ? ASPECT RATIO new");
			v.dataset.aspectRatio = aspectRatio;
			pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
			setTimeout(function () {
				updateMixer();
			}, 1);
		}
	});
	if (typeof session.volume == "number") {
		v.volume = session.volume;
	} else {
		v.volume = 1.0; // play audio automatically
	}
	v.autoplay = true;
	v.controls = session.showControls || false;
	v.classList.add("tile");
	v.setAttribute("playsinline", "");
	v.controlTimer = null;

	v.dataset.menu = "context-menu-video";
	if (!session.cleanOutput) {
		v.classList.add("task"); // this adds the right-click menu
	}

	if (document.getElementById("mainmenu")) {
		var m = getById("mainmenu");
		m.remove();
		document.querySelectorAll(".hidden2").forEach(ele2 => {
			ele2.classList.remove("hidden2");
		});
	}

	if (session.director) {
		if (session.showControls !== null) {
			v.controls = session.showControls;
		} else {
			v.controls = true;
		}
		var container = getById("screenContainer_" + UUID);
		v.container = container;
		v.disablePictureInPicture = false;
		v.setAttribute("controls", "controls");
		container.appendChild(v);
		pokeIframeAPI("control-box-video-updated", v.id, UUID);
		session.requestRateLimit(session.directorViewBitrate, UUID); /// limit resolution for director
		v.title = "Hold CTRL or CMD (⌘) while clicking the video to open detailed stats";
		if (session.beepToNotify) {
			playtone();
		}
	} else if (session.scene !== false) {
		v.controls = session.showControls || false;

		if (session.view) {
			// specific video to be played
			v.style.display = "block";
		} else if (session.scene === "0") {
			// auto plays, right?
			v.style.display = "block";
		} else {
			// group scene I guess; needs to be added manually
			v.style.display = "none";
			v.mutedStateScene = true;
		}

		setTimeout(function () {
			updateMixer();
		}, 1);
	} else if (session.roomid !== false) {
		if (session.cleanOutput) {
			v.controls = session.showControls || false;
		} else if (session.studioSoftware) {
			v.controls = session.showControls || false;
		} else if (session.showControls !== null) {
			v.controls = session.showControls;
		} else {
			v.controls = true;
		}
		//if ((session.roomid==="") && (session.bitrate)){
		// let's keep the default bitrates, since this isn't a real room and bitrates are specified.
		//} //else if (session.novideo !== false){
		//	if (session.novideo.includes(session.rpcs[UUID].streamID)){ // no video will have muted the video already anyways.
		//		session.requestRateLimit(0,UUID, false);//  optimizing audio here doesn't later get turned back on.  let the automixer disable audio instead
		//	}
		//} //else {
		//	session.requestRateLimit(0,UUID, false);////  optimizing audio here doesn't later get turned back on.  let the automixer disable audio instead
		//}
		setTimeout(function () {
			updateMixer();
		}, 1);
	} else {
		v.style.display = "block";
		setTimeout(function () {
			updateMixer();
		}, 1);
	}

	v.addEventListener("click", function (e) {
		// show stats of video if double clicked
		log("clicked");
		try {
			var uid = e.currentTarget.dataset.UUID;
			if (e.ctrlKey || e.metaKey) {
				e.preventDefault();
				if (session.statsMenu !== false) {
					if ("stats" in session.rpcs[uid]) {
						var [menu, innerMenu] = statsMenuCreator();
						printViewStats(innerMenu, uid);
						menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
					}
				}
				e.stopPropagation();
				return false;
			} else if ("prePausedBandwidth" in session.rpcs[uid]) {
				unPauseVideo(e.currentTarget);
			}
		} catch (e) {
			errorlog(e);
		}
	});

	if (session.statsMenu) {
		if ("stats" in session.rpcs[UUID]) {
			if (getById("menuStatsBox")) {
				clearInterval(getById("menuStatsBox").interval);
				getById("menuStatsBox").remove();
			}

			var [menu, innerMenu] = statsMenuCreator();
			printViewStats(innerMenu, UUID);
			menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID);
		}
	}

	v.touchTimeOut = null;
	v.touchLastTap = 0;
	v.touchCount = 0;
	v.addEventListener("touchend", function (event) {
		if (session.disableMouseEvents) {
			return;
		}

		log("touched");

		//document.ontouchup = null;
		//document.onmouseup = null;
		document.onmousemove = null;
		document.ontouchmove = null;

		var currentTime = new Date().getTime();
		var tapLength = currentTime - v.touchLastTap;
		clearTimeout(v.touchTimeOut);
		if (tapLength < 500 && tapLength > 0) {
			///
			log("double touched");
			v.touchCount += 1;
			event.preventDefault();
			if (v.touchCount < 5) {
				v.touchLastTap = currentTime;
				return false;
			}
			v.touchLastTap = 0;
			v.touchCount = 0;

			log("double touched");
			if (session.statsMenu !== false) {
				var uid = event.currentTarget.dataset.UUID;
				if ("stats" in session.rpcs[uid]) {
					var [menu, innerMenu] = statsMenuCreator();

					printViewStats(innerMenu, uid);

					menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
				}
			}
			event.stopPropagation();
			return false;
			//////
		} else {
			v.touchCount = 1;
			v.touchTimeOut = setTimeout(
				function (vv) {
					clearTimeout(vv.touchTimeOut);
					vv.touchLastTap = 0;
					vv.touchCount = 0;
				},
				5000,
				v
			);
			v.touchLastTap = currentTime;
		}
	});

	if (v.controls == false) {
		v.addEventListener("click", function () {
			if (v.paused) {
				log("PLAYING MANUALLY?");
				v.play()
					.then(_ => {
						log("playing 5");
					})
					.catch(warnlog);
			}
		});
		if (session.nocursor == false) {
			// we do not want to show the controls. This is because MacOS + OBS does not work; so electron app needs this.
			if (!session.cleanOutput) {
				if (session.studioSoftware) {
				} else if (session.showControls === false) {
					// explicitly disabled; default null.
				} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
				} else {
					if (v.controlTimer) {
						clearInterval(v.controlTimer);
					}
					v.controlTimer = setTimeout(showControlBar.bind(null, v), 1000);
					//v.controlTimer = setTimeout(function (){v.controls=true;},3000); // 3 seconds before I enable the controls automatically. This way it doesn't auto appear during loading.  3s enough, right?
				}
			}
		}
	}

	//if (session.fadein){
	v.addEventListener("animationend", function (e) {
		v.classList.remove("fadein"); // allows the video to fade in.
		if (v.holder) {
			v.holder.classList.remove("fadein");
		}
	});
	//	v.classList.add("fadein"); // allows the video to fade in.
	//	if (v.holder){
	//		v.holder.classList.add("fadein");
	//	}
	//}

	applyMuteState(UUID); // TODO; needs to be specific to screen video
	v.usermuted = false;

	v.addEventListener("volumechange", function (e) {
		var muteState = checkMuteState(UUID);
		if (this.muted && this.muted !== muteState) {
			this.usermuted = 1;
		} else if (!this.muted && this.muted !== muteState) {
			this.usermuted = 2;
		} else if (!this.muted) {
			this.usermuted = false;
		}
	});

	if (session.screenShareStartPaused) {
		// we know this is a screen share already
		pauseVideo(v, false);
	}

	if (session.director) {
		/* var wss = "";
		if (session.customWSS || session.wssSetViaUrl){
			if (session.customWSS && (session.customWSS!==true)){
				wss = "&pie="+session.customWSS;
			} else if (session.customWSS==true){
				wss = "&wss=" + session.wss;
			} else {
				wss = "&wss2=" + session.wss;
			}
		} */
		/* 
		var codecGroupFlag="";
		if (session.codecGroupFlag){
			codecGroupFlag = session.codecGroupFlag;
		} */

		/* var passAdd2="";
		if (session.password){
			if (session.defaultPassword===false){
				passAdd2="&password="+session.password;
			}
		} */

		if (session.customWSS && "isScene" in msg && msg.isScene !== false) {
			// this is a scene, so lets not show it.
		} else {
			var soloLink = soloLinkGenerator(session.rpcs[UUID].streamID);
			createControlBoxScreenshare(UUID, soloLink, session.rpcs[UUID].streamID);
		}
	}

	if (session.autorecord || session.autorecordremote) {
		log("AUTO RECORD START");
		setTimeout(
			function (UUID, v) {
				var videoKbps = session.recordDefault;
				if (session.recordLocal !== false) {
					videoKbps = session.recordLocal;
				}

				if (session.director) {
					recordVideo(document.querySelector("[data-action-type='recorder-local'][data--u-u-i-d='" + UUID + "']"), null, videoKbps);
				} else if (v.stopWriter || v.recording) {
				} else if (v.startWriter) {
					v.startWriter();
				} else {
					recordLocalVideo(null, videoKbps, v);
				}
			},
			2000,
			UUID,
			v
		);
	}
	if (session.rpcs[UUID]) {
		clearTimeout(session.rpcs[UUID].getStatsTimeout);
		session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, 100, UUID);
	}
}

function setupIncomingVideoTracking(v, UUID) {
	// video element.

	if (session.directorList.indexOf(UUID) >= 0) {
		v.muted = false;
	}

	v.onpause = event => {
		// prevent things from pausing; human or other

		if (v.dataset.UUID && session.rpcs[v.dataset.UUID] && session.rpcs[v.dataset.UUID].manualBandwidth === 0) {
			return true;
		}

		if (!CtrlPressed) {
			warnlog("Video paused; force it to play again");
			//return;
			//session.audioCtx.resume();
			//log("ctx resume");

			event.currentTarget
				.play()
				.then(_ => {
					log("playing 6");
				})
				.catch(error => {
					warnlog("didnt play 1");
				});
			unPauseVideo(v);
		} else if (Firefox && CtrlPressed) {
			log("CLICK 351");
			if (session.statsMenu !== false) {
				var uid = event.currentTarget.dataset.UUID;
				event.preventDefault();
				if ("stats" in session.rpcs[uid]) {
					var [menu, innerMenu] = statsMenuCreator();

					printViewStats(innerMenu, uid);

					menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
				}
			}
			event.stopPropagation();
			return false;
		}
		return true;
	};

	/* v.onerror = function(event){
		errorlog(event);
		try{
			warnlog("Vidieo element threw an error; going to reconnect it");
			session.rpcs[UUID].videoElement.stop();
			session.rpcs[UUID].videoElement.srcObject = null;
			session.rpcs[UUID].videoElement.srcObject = session.rpcs[UUID].streamSrc;  // replaecd with  updateIncomingVideoElement these days
			session.rpcs[UUID].videoElement.play();
			setTimeout(function(){updateMixer();},1);
		}  catch(e){errorlog(e);}
	} */

	if (session.pip) {
		v.onloadedmetadata = function () {
			if (!v.paused) {
				if (!v.pip) {
					v.pip = true;
					toggleSystemPip(v, true);
				}
			}
		};
	}

	v.addEventListener("resize", e => {
		var v = e.target;
		var aspectRatio = parseFloat(v.videoWidth / v.videoHeight) || 0;
		log("resize event: " + aspectRatio);

		if (!aspectRatio) {
			v.resetAR = true;
			return;
		} // if Audio only, then we don't want to set or update any aspect ratio.
		if (typeof v.manualRotate == "number"){
			//v.rotated = v.manualRotate; // ((session.rotate || 0) + 90) % 360;
		} else if (session.keepIncomingVideosInLandscape) {
			if (aspectRatio < 1) {
				// session.keepIncomingVideosInLandscape
				v.rotated = session.keepIncomingVideosInLandscape;
			} else {
				v.rotated = 0;
			}
		} else if (session.keepIncomingVideosInPortrait) {
			if (aspectRatio > 1) {
				// session.keepIncomingVideosInLandscape
				v.rotated = session.keepIncomingVideosInPortrait;
			} else {
				v.rotated = 0;
			}
		}

		if (v.resetAR) {
			log("ASPECT RATIO UNMUTED");
			delete v.resetAR;
			v.dataset.aspectRatio = aspectRatio;
			pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
			setTimeout(function () {
				updateMixer();
			}, 1);
		} else if (v.dataset.aspectRatio) {
			if (aspectRatio != parseFloat(v.dataset.aspectRatio)) {
				log("ASPECT RATIO CHANGED");
				v.dataset.aspectRatio = aspectRatio;
				pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
				setTimeout(function () {
					updateMixer();
				}, 1); // We don't want to run this on the first resize?  just subsequent ones.
			}
		} else {
			log("NEW VIDEO ? ASPECT RATIO new");
			v.dataset.aspectRatio = aspectRatio;
			pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
			setTimeout(function () {
				updateMixer();
			}, 1);
		}
	});

	if (typeof session.volume == "number") {
		v.volume = session.volume;
	} else {
		v.volume = 1.0; // play audio automatically
	}
	v.autoplay = true;
	v.controls = session.showControls || false;
	v.classList.add("tile");
	v.setAttribute("playsinline", "");
	v.controlTimer = null;

	v.dataset.menu = "context-menu-video";
	if (!session.cleanOutput) {
		v.classList.add("task"); // this adds the right-click menu
	}

	if (document.getElementById("mainmenu")) {
		var m = getById("mainmenu");
		m.remove();
		document.querySelectorAll(".hidden2").forEach(ele2 => {
			ele2.classList.remove("hidden2");
		});
	}

	if (session.director) {
		if (session.showControls === false) {
			v.controls = false;
		} else {
			v.controls = true;
		}
		var container = getById("videoContainer_" + UUID);
		v.container = container;
		v.disablePictureInPicture = false;
		v.setAttribute("controls", "controls");
		container.appendChild(v);
		pokeIframeAPI("control-box-video-updated", v.id, UUID);
		container.classList.add("hasMedia");
		session.requestRateLimit(session.directorViewBitrate, UUID); /// limit resolution for director
		v.title = "Hold CTRL or CMD (⌘) while clicking the video to open detailed stats";
		if (session.beepToNotify) {
			playtone();
		}
	} else if (session.scene !== false) {
		v.controls = session.showControls || false;

		if (session.view) {
			// specific video to be played
			v.style.display = "block";
		} else if (session.scene === "0") {
			// auto plays, right?
			v.style.display = "block";
		} else if (session.scene !== false && session.autoadd && session.rpcs[UUID].streamID && session.autoadd.includes(session.rpcs[UUID].streamID)) {
			/// session.autoadd
			v.style.display = "block"; // auto added because manually added.
		} else {
			// group scene I guess; needs to be added manually
			v.style.display = "none";
			session.rpcs[UUID].mutedStateScene = true;
		}
	} else if (session.roomid !== false) {
		if (session.cleanOutput) {
			v.controls = session.showControls || false;
		} else if (session.studioSoftware) {
			v.controls = session.showControls || false;
		} else if (session.showControls !== null) {
			v.controls = session.showControls;
		} else {
			v.controls = true;
		}
	} else {
		v.style.display = "block";
	}

	v.addEventListener("click", function (e) {
		// show stats of video if double clicked
		log("clicked");
		try {
			var uid = e.currentTarget.dataset.UUID;
			if (e.ctrlKey || e.metaKey) {
				e.preventDefault();
				if (session.statsMenu !== false) {
					if ("stats" in session.rpcs[uid]) {
						var [menu, innerMenu] = statsMenuCreator();
						printViewStats(innerMenu, uid);

						menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
					}
				}
				e.stopPropagation();
				return false;
			} else if ("prePausedBandwidth" in session.rpcs[uid]) {
				unPauseVideo(e.currentTarget);
			}
		} catch (e) {
			errorlog(e);
		}
	});

	if (session.statsMenu) {
		if ("stats" in session.rpcs[UUID]) {
			if (getById("menuStatsBox")) {
				clearInterval(getById("menuStatsBox").interval);
				getById("menuStatsBox").remove();
			}

			var [menu, innerMenu] = statsMenuCreator();
			printViewStats(innerMenu, UUID);
			menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID);
		}
	}

	v.touchTimeOut = null;
	v.touchLastTap = 0;
	v.touchCount = 0;
	v.addEventListener("touchend", function (event) {
		if (session.disableMouseEvents) {
			return;
		}

		log("touched");

		//document.ontouchup = null;
		//document.onmouseup = null;
		document.onmousemove = null;
		document.ontouchmove = null;

		var currentTime = new Date().getTime();
		var tapLength = currentTime - v.touchLastTap;
		clearTimeout(v.touchTimeOut);
		if (tapLength < 500 && tapLength > 0) {
			///
			log("double touched");
			v.touchCount += 1;
			event.preventDefault();
			if (v.touchCount < 5) {
				v.touchLastTap = currentTime;
				return false;
			}
			v.touchLastTap = 0;
			v.touchCount = 0;

			log("double touched");
			if (session.statsMenu !== false) {
				var uid = event.currentTarget.dataset.UUID;
				if ("stats" in session.rpcs[uid]) {
					var [menu, innerMenu] = statsMenuCreator();
					printViewStats(innerMenu, uid);
					menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
				}
			}
			event.stopPropagation();
			return false;
			//////
		} else {
			v.touchCount = 1;
			v.touchTimeOut = setTimeout(
				function (vv) {
					clearTimeout(vv.touchTimeOut);
					vv.touchLastTap = 0;
					vv.touchCount = 0;
				},
				5000,
				v
			);
			v.touchLastTap = currentTime;
		}
	});

	if (session.rpcs[UUID].stats.info && "remote" in session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote) {
		v.addEventListener("wheel", remotePTZRequest);
		// v.addEventListener("wheel", remoteFocusZoomRequest); //  just remote focus -- obsolete.
	}
	
	if (session.ptzSlider && (session.director || (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote))) {
		
		const ptzContainer = document.createElement('div');
		ptzContainer.className = 'video-ptz-controls';
		
		// Zoom slider
		const zoomSlider = document.createElement('div');
		zoomSlider.className = 'video-zoom-slider';
		
		const zoomLabel = document.createElement('label');
		zoomLabel.innerText = "Zoom";
		
		const zoomInput = document.createElement('input');
		zoomInput.title = "Camera zoom control";
		zoomInput.type = 'range';
		zoomInput.min = '0';
		zoomInput.max = '100';
		zoomInput.value = '0';
		
		let zoomUpdating = false;
		zoomInput.addEventListener('input', (e) => {
			if (zoomUpdating) return;
			const zoomValue = parseInt(e.target.value) / 100;  // Normalize to 0-1
			
			session.requestZoomChange(zoomValue, UUID, session.remote, true);
		});
		
		// Pan slider
		const panSlider = document.createElement('div');
		panSlider.className = 'video-pan-slider';
		
		const panLabel = document.createElement('label');
		panLabel.innerText = "Pan";
		
		const panInput = document.createElement('input');
		panInput.title = "Camera pan control (left/right)";
		panInput.type = 'range';
		panInput.min = '-100';
		panInput.max = '100';
		panInput.value = '0';
		
		let panUpdating = false;
		panInput.addEventListener('input', (e) => {
			if (panUpdating) return;
			const panValue = parseInt(e.target.value) / 100;  // Normalize to -1 to 1
			
			session.requestPanChange(panValue, UUID, session.remote, true);
		});
		
		// Tilt slider
		const tiltSlider = document.createElement('div');
		tiltSlider.className = 'video-tilt-slider';
		
		const tiltLabel = document.createElement('label');
		tiltLabel.innerText = "Tilt";
		
		const tiltInput = document.createElement('input');
		tiltInput.title = "Camera tilt control (up/down)";
		tiltInput.type = 'range';
		tiltInput.min = '-100';
		tiltInput.max = '100';
		tiltInput.value = '0';
		
		let tiltUpdating = false;
		tiltInput.addEventListener('input', (e) => {
			if (tiltUpdating) return;
			const tiltValue = parseInt(e.target.value) / 100;  // Normalize to -1 to 1
			
			session.requestTiltChange(tiltValue, UUID, session.remote, true);
		});
		
		// Append elements to containers
		zoomSlider.appendChild(zoomLabel);
		zoomSlider.appendChild(zoomInput);
		
		panSlider.appendChild(panLabel);
		panSlider.appendChild(panInput);
		
		tiltSlider.appendChild(tiltLabel);
		tiltSlider.appendChild(tiltInput);
		
		ptzContainer.appendChild(zoomSlider);
		ptzContainer.appendChild(panSlider);
		ptzContainer.appendChild(tiltSlider);
		
		if (!v.container) {
			v.container = getById("videoContainer_" + UUID);
		}
		
		v.container.appendChild(ptzContainer);
		
		// Store references for external updates
		session.rpcs[UUID].zoomSlider = (value) => {
			zoomUpdating = true;
			zoomInput.value = Math.round(value * 100);  // Convert 0-1 to 0-100
			zoomUpdating = false;
		};
		
		session.rpcs[UUID].panSlider = (value) => {
			panUpdating = true;
			panInput.value = Math.round(value * 100);  // Convert -1 to 1 to -100 to 100
			panUpdating = false;
		};
		
		session.rpcs[UUID].tiltSlider = (value) => {
			tiltUpdating = true;
			tiltInput.value = Math.round(value * 100);  // Convert -1 to 1 to -100 to 100
			tiltUpdating = false;
		};
	} else if (session.zoomSlider && (session.director || (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote))){
		const slider = document.createElement('div');
		slider.className = 'video-zoom-slider0';
		
		const input = document.createElement('input');
		input.title = "Hint: The remote camera's browser may needs to be visible for zoom to work in certain browsers";
		input.type = 'range';
		input.min = '0';
		input.max = '255';
		input.value = '0';
		
		let updating = false;
		input.addEventListener('input', (e) => {
			if (updating) return;
			const zoomValue = parseInt(e.target.value) / 255;
			session.requestZoomChange(zoomValue, UUID, session.remote, true);
		});
		
		// Update slider if remote changes occur
		const updateSlider = (value) => {
			updating = true;
			input.value = Math.round(value * 255);
			updating = false;
			input.title = input.value*100 + "";
		};
		
		slider.appendChild(input);
		
		if (!v.container){
			v.container = getById("videoContainer_" + UUID);
		}
		
		v.container.appendChild(slider);
		
		// Store reference for external updates
		session.rpcs[UUID].zoomSlider = updateSlider;
	}

	if (v.controls == false) {
		v.addEventListener("click", function () {
			log("click 33");
			if (v.paused) {
				log("PLAYING MANUALLY?");
				v.play()
					.then(_ => {
						log("playing 7");
					})
					.catch(warnlog);
			}
		});
		if (session.nocursor == false) {
			// we do not want to show the controls. This is because MacOS + OBS does not work; so electron app needs this.
			if (!session.cleanOutput) {
				if (session.studioSoftware) {
				} else if (session.showControls === false) {
					// explicitly disabled; default null.
				} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
				} else {
					if (v.controlTimer) {
						clearInterval(v.controlTimer);
					}
					v.controlTimer = setTimeout(showControlBar.bind(null, v), 1000);
					//v.controlTimer = setTimeout(function (){v.controls=true;},3000); // 3 seconds before I enable the controls automatically. This way it doesn't auto appear during loading.  3s enough, right?
				}
			}
		}
	}

	//if (session.fadein){
	v.addEventListener("animationend", function (e) {
		v.classList.remove("fadein"); // allows the video to fade in.
		if (v.holder) {
			v.holder.classList.remove("fadein");
		}
	});
	//v.classList.add("fadein"); // allows the video to fade in.
	//	if (v.holder){
	////		v.holder.classList.add("fadein");
	//	}
	//}

	applyMuteState(UUID);
	v.usermuted = false;

	if (session.screenShareStartPaused && session.rpcs[UUID].screenShareState) {
		pauseVideo(v, false);
	}

	v.addEventListener("volumechange", function (e) {
		var muteState = checkMuteState(UUID);
		if (this.muted && this.muted !== muteState) {
			this.usermuted = 1;
		} else if (!this.muted && this.muted !== muteState) {
			this.usermuted = 2;
		} else if (!this.muted) {
			this.usermuted = false;
		}
	});

	if (session.autorecord || session.autorecordremote) {
		log("AUTO RECORD START");
		setTimeout(
			function (UUID, v) {
				var videoKbps = session.recordDefault;
				if (session.recordLocal !== false) {
					videoKbps = session.recordLocal;
				}

				if (session.director) {
					recordVideo(document.querySelector("[data-action-type='recorder-local'][data--u-u-i-d='" + UUID + "']"), null, videoKbps);
				} else if (v.stopWriter || v.recording) {
				} else if (v.startWriter) {
					v.startWriter();
				} else {
					recordLocalVideo(null, videoKbps, v);
				}
			},
			2000,
			UUID,
			v
		);
	}

	if (session.rpcs[UUID]) {
		clearTimeout(session.rpcs[UUID].getStatsTimeout);
		session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, 100, UUID);
	}
}

session.requestPanChange = async function(pan, UUID, passwd = session.remote, absolute=false) {
    // pan is now expected to be a value between -1 and 1
    log("request pan change: " + pan);
    var msg = {};
    msg.pan = pan;  // Normalized value -1 to 1
    msg.remote = passwd;
	msg.abs = absolute;
    msg = await session.encodeRemote(msg);
    if (session.sendRequest(msg, UUID)) {
        log("pan success");
        return true;
    } else {
        errorlog("failed to send pan change request");
        return false;
    }
};

session.requestTiltChange = async function(tilt, UUID, passwd = session.remote, absolute=false) {
    // tilt is now expected to be a value between -1 and 1
    log("request tilt change: " + tilt);
    var msg = {};
    msg.tilt = tilt;  // Normalized value -1 to 1
    msg.remote = passwd;
	msg.abs = absolute;
    msg = await session.encodeRemote(msg);
    if (session.sendRequest(msg, UUID)) {
        log("tilt success");
        return true;
    } else {
        errorlog("failed to send tilt change request");
        return false;
    }
};

session.requestZoomChange = async function(zoom, UUID, passwd = session.remote, absolute=false) {
    // zoom is now expected to be a value between 0 and 1
    log("request zoom change: " + zoom);
    var msg = {};
    msg.zoom = zoom;  // Normalized value 0 to 1
    msg.abs = absolute;
    msg.remote = passwd;
    msg = await session.encodeRemote(msg);
    if (session.sendRequest(msg, UUID)) {
        log("zoom success");
        return true;
    } else {
        errorlog("failed to send zoom change request");
        return false;
    }
};

session.requestFocusChange = async function (focal, UUID, passwd = session.remote, absolute=false) {
	log("request focus change: " + focal);

	var msg = {};
	msg.focus = focal;
	msg.abs = absolute;
	msg.remote = passwd;
	msg = await session.encodeRemote(msg);

	if (session.sendRequest(msg, UUID)) {
		log("focus success");
	} else {
		errorlog("failed to send focus change request");
	}
};

function remotePTZRequest(event) {
    event.preventDefault();

    var scale = event.deltaY > 0 ? -0.05 : 0.05;  // Use larger normalized steps

    if (!event.altKey) {
        scale *= 2;  // Double the scale when not holding Alt
    }

    if (event.ctrlKey || event.metaKey) {
        if (event.shiftKey) {
            // tilt: -1 to 1
            session.requestTiltChange(scale, event.currentTarget.dataset.UUID);
        } else {
            // focus: -1 to 1
            session.requestFocusChange(scale, event.currentTarget.dataset.UUID);
        }
    } else if (event.shiftKey) {
        // pan: -1 to 1
        session.requestPanChange(scale, event.currentTarget.dataset.UUID);
    } else {
        // zoom: 0 to 1 (relative)
        session.requestZoomChange(scale, event.currentTarget.dataset.UUID);
    }
}


function remoteFocusZoomRequest(event) { // obsolete.
	event.preventDefault();
	
	var scale = event.deltaY> 0 ? -0.004 : 0.004;
	log(event.currentTarget);
	log(event.deltaY);
	
	if (!event.altKey){
		scale*=10;
	}

	if (event.ctrlKey || event.metaKey) {
		// focus
		session.requestFocusChange(scale, event.currentTarget.dataset.UUID);
	} else {
		// zoom
		session.requestZoomChange(scale, event.currentTarget.dataset.UUID);
	}
}

function mediaAudioTrackUpdated(UUID, streamID) {
	pokeIframeAPI("new-audio-track-added", true, UUID, streamID); //  videoTrack is whether video. audio will be false I guess.
}
function mediaVideoTrackUpdated(UUID, streamID) {
	pokeIframeAPI("new-video-track-added", true, UUID, streamID); //  videoTrack is whether video. audio will be false I guess.
}
function mediaSourceUpdated(UUID, streamID) {
	pokeIframeAPI("new-stream-added", true, UUID, streamID); //  videoTrack is whether video. audio will be false I guess.
	pokeAPI("streamAdded", streamID);
}

function showControlBar(vel) {
	try {
		vel.controls = true;
	} catch (e) {
		errorlog(e);
	}
}

function createRichVideoElement(UUID) {
	// this function is used to check and generate a rich video element if needed
	if (!session.rpcs[UUID].videoElement) {
		log("video element is being created and any media tracks added");
		session.rpcs[UUID].videoElement = createVideoElement();
		session.rpcs[UUID].videoElement.dataset.UUID = UUID;
		session.rpcs[UUID].videoElement.id = "videosource_" + UUID; // could be set to UUID in the future

		if (session.rpcs[UUID].streamID) {
			session.rpcs[UUID].videoElement.dataset.sid = session.rpcs[UUID].streamID;
		}

		if (session.rpcs[UUID].rotate !== false) {
			session.rpcs[UUID].videoElement.rotated = session.rpcs[UUID].rotate;
			session.rpcs[UUID].videoElement.dataset.rotated = session.rpcs[UUID].rotate;
		}

		session.rpcs[UUID].videoElement.addEventListener(
			"playing",
			e => {
				try {
					var bigPlayButton = document.getElementById("bigPlayButton");
					if (bigPlayButton) {
						bigPlayButton.parentNode.removeChild(bigPlayButton);
					}
				} catch (e) {}

				resetupAudioOut(e.target, true);

				try {
					if (session.pip) {
						if (v.readyState >= 3) {
							if (!v.pip) {
								v.pip = true;
								toggleSystemPip(v, true);
							}
						}
					}
				} catch (e) {}
			},
			{ once: true }
		);

		if (session.rpcs[UUID].mirrorState) {
			applyMirrorGuest(session.rpcs[UUID].mirrorState, session.rpcs[UUID].videoElement);
		} else if (session.rpcs[UUID].mirrorState === false) {
			applyMirrorGuest(session.rpcs[UUID].mirrorState, session.rpcs[UUID].videoElement);
		}

		if (session.posterImage) {
			session.rpcs[UUID].videoElement.poster = session.posterImage;
		}

		setupIncomingVideoTracking(session.rpcs[UUID].videoElement, UUID);
		pokeIframeAPI("video-element-created", "videosource_" + UUID, UUID);
	}
	return session.rpcs[UUID].videoElement;
}

function updateVolume(update = false) {
	if (session.audioGain !== false) {
		if (update) {
			if (session.roomid) {
				var pswd = session.password || "";
				generateHash(session.streamID + session.roomid + pswd + session.salt, 6).then(function (hash) {
					setStorage("micVolume_" + hash, session.audioGain, (hours = 6));
				});
			}
		}
		if (session.audioGain === 0) {
			getById("header").classList.add("orange");
			getById("head7").classList.remove("hidden");
		} else {
			getById("header").classList.remove("orange");
			getById("head7").classList.add("hidden");
		}
	} else {
		var pswd = session.password || "";
		generateHash(session.streamID + session.roomid + pswd + session.salt, 6).then(function (hash) {
			var volume = getStorage("micVolume_" + hash);
			if (volume !== "") {
				if (parseInt(volume) === 0) {
					getById("header").classList.add("orange");
					getById("head7").classList.remove("hidden");
				} else if (parseInt(volume)) {
					getById("header").classList.remove("orange");
					getById("head7").classList.add("hidden");
				} else {
					return;
				}
				session.audioGain = parseInt(volume);
				var vol = parseFloat(session.audioGain / 100) || 0;
				for (var waid in session.webAudios) {
					// TODO:  EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (trackid === wa.id){..
					log("Adjusting Gain; only track 0 in all likely hood, unless more than track 0 support is added.");
					session.webAudios[waid].gainNode.gain.setValueAtTime(vol, session.webAudios[waid].audioContext.currentTime);
				}
			}
		});
	}
}

function hideHomeCheck() {
	if (session.hidehome) {
		getById("logoname").classList.add("permahide");
		getById("container-1").classList.add("permahide");
		getById("container-4").classList.add("permahide");
		getById("dropButton").classList.add("permahide");
		getById("head1").classList.add("permahide");
		if (session.permaid === false && session.roomid == false && !session.webcamonly && !session.screenshare) {
			getById("mainmenu").classList.add("permahide");
		} else {
			getById("mainmenu").classList.remove("permahide");
		}

		getById("audioScreenCaptureDocs").classList.add("permahide");
		getById("audioScreenCaptureDocs2").classList.add("permahide");
		getById("translateButton").classList.add("permahide");
		getById("calendarButton").classList.add("permahide");
		getById("info").classList.add("permahide");
		getById("helpbutton").classList.add("permahide");
	}

	if (urlParams.has("headertitle")) {
		let pageTitle = urlParams.get("headertitle") || "";
		pageTitle = decodeURIComponent(pageTitle) || "";
		document.title = pageTitle;
		getById("metaTitle").content = pageTitle;
	}

	if (urlParams.has("favicon")) {
		let favicon = "";
		if (urlParams.get("favicon")) {
			favicon = decodeURIComponent(urlParams.get("favicon")) || "";
		}
		getById("favicon1").href = favicon;
		getById("favicon2").href = favicon;
		getById("favicon3").href = favicon;
	}
}

function stashRoomSession(broadcastFlag = null) {
	try {
		let settings = {};

		settings.roomid = session.roomid;
		settings.password = session.password;
		settings.label = session.label;
		settings.trb = session.totalRoomBitrate;
		settings.widget = session.widget;
		settings.codecGroupFlag = session.codecGroupFlag;
		settings.showDirector = session.showDirector;

		if (broadcastFlag !== null) {
			settings.broadcast = broadcastFlag;
		}

		setStorage("directorOtherSettings", settings);
	} catch (e) {
		errorlog(e);
	}
}

// toggleQualityDirector(1200, this.dataset.UUID, this)

function switchModes(state = null) {
	if (state === null) {
		session.switchMode = !session.switchMode;
	} else {
		session.switchMode = state;
	}
	if (session.switchMode) {
		getById("directorlayout").classList.add("hidden");
		getById("gridlayout").classList.remove("hidden");
		updateMixer();
	} else {
		getById("directorlayout").classList.remove("hidden");
		getById("gridlayout").classList.add("hidden");

		for (var UUID in session.rpcs) {
			if (session.rpcs[UUID].videoElement) {
				session.rpcs[UUID].videoElement.style = "";
				session.rpcs[UUID].videoElement.alreadyAdded = false;
				var target = document.querySelector("#container_" + UUID + " .controlVideoBox");
				if (target) {
					target.prepend(session.rpcs[UUID].videoElement);
					if (session.signalMeter) {
						if (session.rpcs[UUID].signalMeter) {
							target.appendChild(session.rpcs[UUID].signalMeter);
						}
					}
					if (session.batteryMeter) {
						if (session.rpcs[UUID].batteryMeter) {
							target.appendChild(session.rpcs[UUID].batteryMeter);
						}
					}
					if (session.rpcs[UUID].voiceMeter) {
						target.appendChild(session.rpcs[UUID].voiceMeter);
					}
					if (session.rpcs[UUID].remoteMuteElement) {
						target.appendChild(session.rpcs[UUID].remoteMuteElement);
					}
				}
			}
		}

		if (session.videoElement) {
			session.videoElement.style = "";
			session.videoElement.alreadyAdded = false;

			if (session.showDirector == true) {
				var target = document.querySelector("#videoContainer_director");
				if (target && session.videoElement) {
					target.prepend(session.videoElement);
				}
			} else if ((session.videoElement.srcObject && session.videoElement.srcObject.getTracks().length) || getById("press2talk").dataset.enabled == true) {
				getById("miniPerformer").prepend(session.videoElement);
			}
		}

		if (session.screenShareElement) {
			session.screenShareElement.style = "";
			session.screenShareElement.alreadyAdded = false;

			if (session.showDirector == true) {
				var target = document.querySelector("#videoScreenContainer_director");
				if (target && session.screenShareElement && session.screenShareElement.srcObject && session.screenShareElement.srcObject.getTracks().length) {
					target.prepend(session.screenShareElement);
				}
			} else if ((session.screenShareElement.srcObject && session.screenShareElement.srcObject.getTracks().length) || getById("press2talk").dataset.enabled == true) {
				getById("miniPerformer").prepend(session.videoElement);
			}
		}

		applyQualityDirector();
	}
}

var updateMixerTimer = null;
var updateMixerActive = false;
function updateMixer(e = false) {
	var controlBar = document.getElementById("subControlButtons");
	if (controlBar && controlBar.dragElement && !controlBar.isDragging) {
		let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
		let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);

		// Calculate real boundaries (parent position: fixed issues)
		let topOffset = 0;
		let leftOffset = 0;
		let elementOffset = controlBar;
		while (elementOffset) {
			topOffset += elementOffset.offsetTop;
			leftOffset += elementOffset.offsetLeft;
			elementOffset = elementOffset.offsetParent;
		}

		let realX = controlBar.xOffset + leftOffset;
		let realY = controlBar.yOffset + topOffset;
		let maxX = vw - controlBar.offsetWidth;
		let maxY = vh - controlBar.offsetHeight;

		if (realX > maxX) {
			controlBar.xOffset = maxX - leftOffset;
		} else if (realX < 0) {
			controlBar.xOffset = 0 - leftOffset;
		}

		if (realY > maxY) {
			controlBar.yOffset = maxY - topOffset;
		} else if (realY < 0) {
			controlBar.yOffset = 0 - topOffset;
		}

		controlBar.style.transform = `translate(${controlBar.xOffset}px, ${controlBar.yOffset}px)`;
	}

	if (session.manual === true) {
		return;
	} else if (!session.switchMode && session.director) {
		return;
	} else if (session.windowed) {
		return;
	}

	clearInterval(updateMixerTimer);
	if (updateMixerActive) {
		if (session.mobile) {
			updateMixerTimer = setTimeout(function () {
				updateMixer();
			}, 200);
		} else {
			updateMixerTimer = setTimeout(function () {
				updateMixer();
			}, 50);
		}
		return;
	}
	updateMixerActive = true;
	log("updating mixer");

	try {
		updateMixerRun(e);
	} catch (e) {}

	if (session.mobile) {
		setTimeout(function () {
			updateMixerActive = false;
		}, 500);
	} else {
		setTimeout(function () {
			updateMixerActive = false;
		}, 100);
	}
}

function updateMixerRun(e = false) {
	// this is the main auto-mixing code.  It's a giant function that runs when there are changes to screensize, video track statuses, etc.
	try {
		if (session.switchMode) {
		} else if (session.director) {
			return;
		} else if (session.manual === true) {
			return;
		}

		var header = getById("header");
		var playarea = getById("gridlayout");

		if (session.pipWindow) {
			var hi = 0;
			var h = session.pipWindow.clientHeight || session.pipWindow.innerHeight || session.pipWindow.outerHeight;
			var w = session.pipWindow.clientWidth || session.pipWindow.innerWidth || session.pipWindow.outerWidth;
		} else if (document.body.dataset.rotated) {
			var hi = header.offsetHeight;
			var w = document.body.clientHeight;

			if (session.widget && session.iFramesAllowed) {
				w *= (100-session.widgetwidth)/100;
				try {
					let widget = document.getElementById("widget");
					if (!widget) {
						widget = document.createElement("iframe");
						widget.id = "widget";
						widget = loadIframe(parseURL4Iframe(session.widget), widget);
						if (widget){
							document.body.appendChild(widget);
							if (session.widgetleft){
								widget.classList.add("left");
								playarea.style.left = session.widgetwidth+"%";
								playarea.style.width = (100 - session.widgetwidth)+"%";
							} else {
								playarea.style.left = "0";
								playarea.style.width = (100 - session.widgetwidth)+"%";
							}
						}
					}
					if (widget){
						widget.style.height = "calc(100% - " + hi + "px)";
						widget.style.top = hi;
					}
				} catch (e) {
					errorlog(e);
				}
			} else if (!session.widget && session.widgetleft){
				playarea.style.left = "0";
			}

			var h = document.body.clientWidth - hi;
			if (session.dedicatedControlBarSpace || document.body.clientWidth <= 700) {
				// # This needs to be reviewed.
				if (session.dedicatedControlBarSpace !== false) {
					let controlBar = document.getElementById("subControlButtons");
					if (controlBar && !session.overlayControls) {
						if (!controlBar.yOffset || controlBar.yOffset > -10) {
							h = document.body.clientWidth - hi - controlBar.offsetHeight;
						}
					}
				}
			}
		} else {
			var hi = header.offsetHeight;
			var w = window.innerWidth;

			if (session.widget && session.iFramesAllowed) {
				w *= (100 - session.widgetwidth)/100;
				try {
					let widget = document.getElementById("widget");
					if (!widget) {
						widget = document.createElement("iframe");
						widget.id = "widget";
						widget = loadIframe(parseURL4Iframe(session.widget), widget);
						if (widget){
							document.body.appendChild(widget);
							
							if (session.widgetleft){
								widget.classList.add("left");
								playarea.style.left = session.widgetwidth+"%";
								playarea.style.width = (100 - session.widgetwidth)+"%";
								playarea.style.position = "absolute";
							} else {
								playarea.style.left = "0";
								playarea.style.width = (100 - session.widgetwidth)+"%";
							}
						}
					}
					if (widget){
						widget.style.height = "calc(100% - " + hi + "px)";
						widget.style.top = hi;
					}
				} catch (e) {
					errorlog(e);
				}
			} else if (!session.widget && session.widgetleft){
				playarea.style.left = "0";
			}

			var h = window.innerHeight - hi;
			if (session.dedicatedControlBarSpace || window.innerHeight <= 700) {
				// # This needs to be reviewed.
				if (session.dedicatedControlBarSpace !== false) {
					let controlBar = document.getElementById("subControlButtons");
					if (controlBar && !session.overlayControls) {
						if (!controlBar.yOffset || controlBar.yOffset > -10) {
							h = window.innerHeight - hi - controlBar.offsetHeight;
						}
					}
				}
			}
		}

		if (session.locked) {
			var w123 = w;
			var h123 = h;

			if (w > h * session.locked) {
				w = h * session.locked;
			} else if (h > w / session.locked) {
				h = w / session.locked;
			}

			playarea.style.left = (w123 - w) / 2 + "px";
			playarea.style.top = (h123 - h) / 2 + "px";
			playarea.style.width = w + "px";
			playarea.style.height = h + "px";
			playarea.style.position = "absolute";
			playarea.style.display = "block";
		}

		var arW = 16.0;
		var arH = 9.0;

		if (session.aspectRatio) {
			if (session.aspectRatio == 1) {
				arW = 9.0;
				arH = 16.0;
			} else if (session.aspectRatio == 2) {
				arW = 12.0; // square root; cause why not.
				arH = 12.0;
			} else if (session.aspectRatio == 3) {
				arW = 12.0; // square root; cause why not.
				arH = 9.0;
			}
		}

		var groups = [...session.group];
		if (session.groupView.length) {
			groups.push(...session.groupView);
		}
		var sssid = false;
		var soloVideo = false;

		if (session.infocus === true) {
			soloVideo = true;
		} else if (session.infocus && session.infocus in session.rpcs) {
			// if the infocus stream is connected
			if (groups.length || session.allowNoGroup) {
				try {
					if (groups.some(item => session.rpcs[session.infocus].group.includes(item))) {
						soloVideo = session.infocus;
					}
				} catch (e) {
					errorlog(e);
				}
			} else {
				soloVideo = session.infocus;
			}
		} else if (session.infocus2 === true) {
			sssid = session.streamID;
		} else if (session.infocus2 && session.infocus2 in session.rpcs) {
			// if the infocus2 stream is connected
			if (groups.length || session.allowNoGroup) {
				try {
					if (groups.some(item => session.rpcs[session.infocus2].group.includes(item))) {
						sssid = session.rpcs[session.infocus2].streamID;
					}
				} catch (e) {
					errorlog(e);
				}
			} else {
				sssid = session.rpcs[session.infocus2].streamID;
			}
		}

		var ww = w / arW;
		var hh = h / arH;

		var mediaPool = [];
		var mediaPool_invisible = [];

		var miniPreview = session.minipreview;

		if (miniPreview && (!session.activeSpeaker && session.layout) && session.streamID && session.streamID in session.layout) {
			miniPreview = false;
			if (session.videoElement.container && session.videoElement.container.id == "minipreview") {
				delete session.videoElement.container;
			}
			if (document.getElementById("minipreview")) {
				document.getElementById("minipreview").remove();
			}
		}

		if (session.iframeEle && session.iframeEle.style.display !== "none") {
			// local feed
			if (session.order !== false) {
				session.iframeEle.order = session.order;
			} else {
				session.iframeEle.order = 0;
			}
			if (session.activeSpeaker && !session.activelySpeaking) {
				mediaPool_invisible.push(session.iframeEle);
			} else {
				mediaPool.push(session.iframeEle);
			}
		}

		if (session.videoElement && (session.videoElement.src || session.videoElement.srcObject)) {
			// I, myself, exist
			if (session.videoElement.style.display !== "none") {
				// local feed
				if (miniPreview && soloVideo !== true) {
					
				} else {
					if (session.order !== false) {
						session.videoElement.order = session.order;
					} else {
						session.videoElement.order = 0;
					}
					if (session.activeSpeaker && !session.activelySpeaking) {
						//mediaPool_invisible.push(session.videoElement);
						//} else if (session.videoElement && session.videoElement.srcObject && (session.videoElement.srcObject.getTracks().length === 0)){
						// do not show a video element if its completely empty.
					} else if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length === 0) {
						// do not show a video element if its completely empty.
					} else if (soloVideo && soloVideo !== true) {
						//
					} else if (session.videoMuted && session.style === 1) {
						// i'm too tired to try to get this working.
					} else {
						mediaPool.push(session.videoElement);
					}
				}
			}
		}

		if (session.screenShareState && session.screenShareElement) {
			// I, myself, exist
			if (!session.screenShareElementHidden) {
				if (session.order !== false) {
					session.screenShareElement.order = session.order;
				} else {
					session.screenShareElement.order = 0;
				}

				if (soloVideo !== false) {
					//session.screenShareElement.style.display="none";
				} else if (session.activeSpeaker && !session.activelySpeaking) {
					//session.screenShareElement.style.display="none";
				} else {
					mediaPool.push(session.screenShareElement);
				}
			}
		}

		var delayedRequestList = {};
		
		function delayedRequestRate(bandwidth, UUID, optimizeAudio = false, lock = null) {
			delayedRequestList[UUID] = [bandwidth, UUID, optimizeAudio, lock];
		}

		if (soloVideo && soloVideo in session.rpcs) {  // this technically can be a scene or guest
			// remote guest being full screened; infocus == UUID
			mediaPool = []; // remove myself from fullscreen

			if (iOS || iPad) {
				if (!miniPreview) {
					miniPreview = 1;
				}
			}

			for (var j in session.rpcs) {
				if (groups.length || session.allowNoGroup) {
					try {
						if (!groups.some(item => session.rpcs[j].group.includes(item))) {
							continue;
						}
					} catch (e) {
						errorlog(e);
					}
				}

				if (j != soloVideo) {
					// this remote guest is NOT in focus
					try {
						if (session.rpcs[j].iframeEle) {
							mediaPool_invisible.push(session.rpcs[j].iframeEle);
						}
						if (session.rpcs[j].videoElement && session.rpcs[j].videoElement.style.display !== "none") {
							// Add it if not hidden
							if (session.scene!==false){
								//delayedRequestRate(session.hiddenSceneViewBitrate, j);
							} else {
								if (document.pictureInPictureElement && document.pictureInPictureElement.id && document.pictureInPictureElement.id == session.rpcs[j].videoElement.id) {
									var bitratePIP = parseInt(session.zoomedBitrate / 4);
									//warnUser("GOOD");
									delayedRequestRate(bitratePIP, j);
								} else {
									delayedRequestRate(0, j); // disable the video of non-fullscreen videos
								}
							}
						} else if (session.rpcs[j].videoElement) {
							delayedRequestRate(0, j, true); // disable the video of non-fullscreen videos
						}
					} catch (e) {
						errorlog(e);
					}
				} else {
					// remote guest is in-focus video
					////////
					try {
						if (session.rpcs[j].iframeEle) {
							mediaPool_invisible.push(session.rpcs[j].iframeEle);
						}
						if (session.rpcs[j].videoElement) {
							
							mediaPool.push(session.rpcs[j].videoElement); // active speaker
							session.rpcs[j].videoElement.style.visibility = "visible";
							
							if (session.rpcs[j].order !== false) {
								session.rpcs[j].videoElement.order = session.rpcs[j].order;
							} else {
								session.rpcs[j].videoElement.order = 0;
							}

							if (session.scene!==false){
								
								
							} else {
								var totalRoomBitrate = session.totalRoomBitrate;
								if (session.controlRoomBitrate !== false && session.controlRoomBitrate !== true) {
									totalRoomBitrate = Math.min(session.controlRoomBitrate, totalRoomBitrate);
								}
								var targetBitrate = session.zoomedBitrate;
								if (totalRoomBitrate > session.zoomedBitrate) {
									targetBitrate = totalRoomBitrate;
								}
								
								delayedRequestRate(targetBitrate, j); // 1.2-Mbps is decent, no? in-focus, so higher bitrate
							}
						}
					} catch (e) {
						errorlog(e);
					}
				}
			}
		} else if (soloVideo && soloVideo === true) { // this cannot be a scene, as you can't have yourself in a scene.  
			// well, fullscreen myself. "true" represents me. UUID would be for others.
			// already added myself to this as fullscreen
			for (var j in session.rpcs) {
				if (groups.length || session.allowNoGroup) {
					try {
						if (!groups.some(item => session.rpcs[j].group.includes(item))) {
							continue;
						}
					} catch (e) {
						errorlog(e);
					}
				}
				try {
					if (session.rpcs[j].videoElement && session.rpcs[j].videoElement.style.display !== "none") {
						// Add it if not hidden
						if (document.pictureInPictureElement && document.pictureInPictureElement.id && document.pictureInPictureElement.id == session.rpcs[j].videoElement.id) {
							var bitratePIP = parseInt(session.zoomedBitrate / 4);
							delayedRequestRate(bitratePIP, j);
							//warnUser("GOOD");
						} else {
							delayedRequestRate(0, j); // disable the video of non-fullscreen videos
						}
						//	mediaPool_invisible.push(session.rpcs[j].videoElement);
					} else if (session.rpcs[j].videoElement) {
						delayedRequestRate(0, j, true); // other videos are disabled when previewing yourself, but audio retained
					}
				} catch (e) {
					errorlog(e);
				}
			}
		} else {
			var roomQuality = 0;
			var screenShareTotal = 0;

			for (var i in session.rpcs) {
				if (session.rpcs[i] === null) {
					continue;
				}
				if (groups.length || session.allowNoGroup) {
					try {
						if (!groups.some(item => session.rpcs[i].group.includes(item))) {
							continue;
						}
					} catch (e) {
						errorlog(e);
					}
				}
				if (session.rpcs[i].videoElement) {
					// remote feeds
					if (session.rpcs[i].videoElement.style.display !== "none") {
						if (session.rpcs[i].videoElement.srcObject && session.rpcs[i].videoElement.srcObject.getVideoTracks().length) {
							// only count videos with actual video tracks; audio-only excluded
							if (session.rpcs[i].videoMuted) {
								// it's video muted
								//	mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on
							} else if (session.rpcs[i].directorVideoMuted) {
								// it's muted by the director, so likely disabled.
								//	mediaPool_invisible.push(session.rpcs[i].videoElement);  // skipped later on
							} else if (session.rpcs[i].virtualHangup) {
							} else if (session.rpcs[i].bandwidthMuted) {
							} else if (session.rpcs[i].videoElement.style.opacity === "0") {
								//	mediaPool_invisible.push(session.rpcs[i].videoElement);  // skipped later on
							} else {
								roomQuality += 1;
								if (session.rpcs[i].screenShareState) {
									screenShareTotal += 1;
								}
							}
						}
					}
				}
			}

			if (session.broadcast !== false) {
				if ((!session.activeSpeaker && session.layout) && session.streamID in session.layout) {
					// skip
				} else {
					if (roomQuality > 0) {
						if (session.nopreview !== false) {
							for (var i = 0; i < mediaPool.length; i++) {
								if (mediaPool[i].nodeName && mediaPool[i].nodeName == "IFRAME") {
									mediaPool[i].style.display = "none";
								}
							}
							mediaPool = []; // we don't want to show our self-preview if in broadcast mode and there is a director.
						}
					}
				}
			}

			if (roomQuality === 0) {
				roomQuality = 1;
			}

			var totalRoomBitrate = session.totalRoomBitrate;

			if (session.controlRoomBitrate !== false && session.controlRoomBitrate !== true) {
				totalRoomBitrate = Math.min(session.controlRoomBitrate, totalRoomBitrate);
			}

			var roomBitrate = totalRoomBitrate;
			var sceneBitrate = false;
			var screenShareBitrate = false;

			if (session.bitrate && !roomBitrate && !session.totalSceneBitrate) {
				roomBitrate = session.bitrate;
			} else if (session.screenShareBitrate !== false) {
				screenShareBitrate = session.screenShareBitrate;
				if (roomQuality - screenShareTotal > 0) {
					roomBitrate = parseInt(totalRoomBitrate / (roomQuality - screenShareTotal));
					if (session.totalSceneBitrate) {
						sceneBitrate = parseInt(session.totalSceneBitrate / (roomQuality - screenShareTotal));
						if (session.bitrate !== false) {
							sceneBitrate = Math.min(session.bitrate, sceneBitrate);
						}
					}
				}
			} else if (screenShareTotal) {
				try {
					if (session.roomid !== false && session.scene === false) {
						if (roomQuality - screenShareTotal <= 0) {
							roomBitrate = totalRoomBitrate;
							screenShareBitrate = totalRoomBitrate;
						} else {
							screenShareBitrate = totalRoomBitrate / (1.5 * screenShareTotal);
							roomBitrate = parseInt((totalRoomBitrate - screenShareBitrate) / (roomQuality - screenShareTotal));
						}
					} else if (session.totalSceneBitrate !== false) {
						if (roomQuality - screenShareTotal <= 0) {
							sceneBitrate = session.totalSceneBitrate;
							if (session.bitrate !== false) {
								sceneBitrate = Math.min(session.bitrate, sceneBitrate);
							}
							screenShareBitrate = sceneBitrate;
						} else {
							screenShareBitrate = parseInt(totalRoomBitrate / (1.5 * screenShareTotal));
							sceneBitrate = parseInt((totalRoomBitrate - screenShareBitrate) / (roomQuality - screenShareTotal));
							if (session.bitrate !== false) {
								sceneBitrate = Math.min(session.bitrate, sceneBitrate);
								screenShareBitrate = Math.min(session.bitrate, screenShareBitrate);
							}
						}
					} else {
						screenShareBitrate = false;
					}
				} catch (e) {
					errorlog(e);
				}
			} else {
				roomBitrate = parseInt(totalRoomBitrate / roomQuality);
				if (session.totalSceneBitrate) {
					sceneBitrate = parseInt(session.totalSceneBitrate / roomQuality);
					if (session.bitrate !== false) {
						sceneBitrate = Math.min(session.bitrate, sceneBitrate);
					}
				}
			}

			if (session.minimumRoomBitrate) {
				if (session.totalRoomBitrate && roomBitrate < session.minimumRoomBitrate) {
					roomBitrate = session.minimumRoomBitrate;
					if (roomBitrate > session.totalRoomBitrate) {
						roomBitrate = session.totalRoomBitrate;
					}
				}
				if (session.totalSceneBitrate && sceneBitrate < session.minimumRoomBitrate) {
					sceneBitrate = session.minimumRoomBitrate;
					if (sceneBitrate > session.totalSceneBitrate) {
						sceneBitrate = session.totalSceneBitrate;
					}
				}
			}

			var i = null;
			var countOrder = 0;
			try {
				var RPCSkeys = Object.keys(session.rpcs); // default sorting type: time added; //RPCSkeys.sort();
			} catch (e) {
				return;
			}

			for (var keyIndex = 0; keyIndex < RPCSkeys.length; keyIndex++) {
				i = RPCSkeys[keyIndex];
				if (session.rpcs[i] === null) {
					continue;
				}
				session.rpcs[i].mutedStateMixer = false;
				if (groups.length || session.allowNoGroup) {
					// The MAIN and LAST group filter.
					try {
						if (!groups.some(item => session.rpcs[i].group.includes(item))) {
							if (session.scene !== false) {
								if (session.groupAudio) {
									delayedRequestRate(session.hiddenSceneViewBitrate, i, false);
								} else {
									delayedRequestRate(session.hiddenSceneViewBitrate, i, true); // hidden. I dont want it to be super low, for video quality reasons.
									session.rpcs[i].mutedStateMixer = true;
								}
								if (!session.hiddenSceneViewBitrate) {
									session.rpcs[i].videoElement.nogb = 2;
								}
							} else {
								if (session.groupAudio) {
									delayedRequestRate(0, i, false);
								} else {
									delayedRequestRate(0, i, true); // w/e   This is not in OBS, so we just set it as low as possible.  Shoudln't exist really unless loading?
									session.rpcs[i].mutedStateMixer = true;
								}
							}
							applyMuteState(i);
							continue;
						}
					} catch (e) {}
				}
				applyMuteState(i);
				var doNotPush = false;

				if (session.rpcs[i].iframeEle) {
					if (session.rpcs[i].iframeEle.style.display == "none") {
						// pass
					} else if (session.rpcs[i].iframeEle.style.opacity === "0") {
						// pass
					} else {
						session.rpcs[i].iframeEle.style.visibility = "visible";

						if (session.rpcs[i].order !== false) {
							session.rpcs[i].iframeEle.order = session.rpcs[i].order;
						} else {
							session.rpcs[i].iframeEle.order = 0;
						}
						try {
							if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
								mediaPool_invisible.push(session.rpcs[i].iframeEle); // TODO: this needs validation; will the iframe be maintained if activer speaker is going? do we even want this?

								/* } else if (session.rpcs[i].iframeEle.dataset.meshcast){ //////// MESH CAST ONLY LOGIC
								if (session.rpcs[i].iframeEle.contentDocument && session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){
									if (session.rpcs[i].iframeVideo){
										mediaPool.push(session.rpcs[i].iframeVideo);
									} else if (session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){
										session.rpcs[i].iframeVideo = session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video")[0];
										session.rpcs[i].iframeVideo.id="meshcast_"+i;
										//errorlog("THIS IS GOOD");
										mediaPool.push(session.rpcs[i].iframeVideo);
									} else {
										//errorlog("No video yet");
									}
								} else { // this is a problem is not on the same domain.
									if (!document.getElementById("iframe_"+i)){
										if (document.getElementById("hiddenElements")){
											document.getElementById("hiddenElements").append(session.rpcs[i].iframeEle);
										} else {
											document.body.append(session.rpcs[i].iframeEle);
										}
										if (session.rpcs[i].iframeVideo){
											mediaPool.push(session.rpcs[i].iframeVideo);
										} else if (session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){
											session.rpcs[i].iframeVideo = session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video")[0];
											session.rpcs[i].iframeVideo.id="meshcast_"+i;
											mediaPool.push(session.rpcs[i].iframeVideo);
										} else {
											//errorlog("No video yet");
										}
											
									} else {
										if (session.rpcs[i].iframeVideo){
											mediaPool.push(session.rpcs[i].iframeVideo);
										} else {
											//errorlog("Does not support contentDocument or something");
										}
									}
								} */
							} else {
								///////// MESH CAST LOGIC ENDS HERE
								//errorlog("not meshcast");
								mediaPool.push(session.rpcs[i].iframeEle);
							}
						} catch (e) {
							errorlog(e);
						}
					}
				}

				if (session.rpcs[i].imageElement) {
					//if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.srcObject.getAudioTracks().length) {
						// is there audio?
						//	mediaPool_invisible.push(session.rpcs[i].videoElement); // include audio as hidden track;
					//}

					if (session.rpcs[i].videoMuted || session.rpcs[i].directorVideoMuted || session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted) {
						continue;
					}

					if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.style.display == "none") {
						// currently this is considered the state of scenes.  pertty dumb on my part.
						continue;
					}

					if (session.rpcs[i].order !== false) {
						session.rpcs[i].imageElement.order = session.rpcs[i].order;
					} else {
						session.rpcs[i].imageElement.order = 0;
					}
					if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
						// mediaPool_invisible.push(session.rpcs[i].imageElement);
					} else {
						mediaPool.push(session.rpcs[i].imageElement);
					}

					doNotPush = true;
				}

				if (session.rpcs[i].videoElement) {
					// remote feeds
					//session.rpcs[i].targetBandwidth = -1;
					if (session.rpcs[i].videoElement.style.opacity === "0") {
						continue;
					}
					try {
						session.rpcs[i].videoElement.style.visibility = "visible";
					} catch (e) {
						errorlog(e);
					}

					if (session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted || session.rpcs[i].directorVideoMuted) {
						continue;
					}

					if (session.style && session.style >= 2) {
						if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) {
							if (session.rpcs[i].videoElement.style.display == "none") {
								// currently this is considered the state of scenes.  pertty dumb on my part.
								continue;
							}

							if (createStyleCanvas(i)) {
								applyStyleEffect(i);
							}

							if (session.rpcs[i].order !== false) {
								session.rpcs[i].canvas.order = session.rpcs[i].order;
							} else {
								session.rpcs[i].canvas.order = 0;
							}
							if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
								//	mediaPool_invisible.push(session.rpcs[i].canvas);
							} else {
								mediaPool.push(session.rpcs[i].canvas);
							}

							doNotPush = true;
							//continue;
						}
					} else if (session.style == 1) {
						if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) {
							//if (session.style==1){ //  avatars and waveforms might be better done elsewhere? as a canvas effect even?
							doNotPush = true;
							//}
						}
					} else if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) {
						if (session.rpcs[i].screenShareState) {
							doNotPush = true;
						}
					}
					//} else if (!session.directorList.indexOf(i)>=0){  // director is never audio-only.  Video if need, yes, but not visualized-audio.
					//	if (session.rpcs[i].videoElement.srcObject && ((session.rpcs[i].videoElement.srcObject.getVideoTracks().length==0) || (session.rpcs[i].videoMuted)) && !session.rpcs[i].directorVideoMuted){
					//		continue;
					//	}
					//}

					session.rpcs[i].opacityMuted = "1";
					if (session.rpcs[i].opacityDisconnect == "1") {
						if (session.rpcs[i].videoElement) {
							session.rpcs[i].videoElement.style.opacity = "1";
						}
					}
					if (session.rpcs[i].videoMuted) {
						if (session.rpcs[i].videoElement.srcObject.getAudioTracks().length == 0) {
							// if no audio track, no point in removing the video track, since it will just stall out then.
							continue; // easiest is to just not show anything if no video and no audio track.
						}
						if (session.rpcs[i].videoElement.srcObject) {
							session.rpcs[i].videoElement.srcObject.getVideoTracks().forEach(track => {
								session.rpcs[i].videoElement.srcObject.removeTrack(track);
								session.rpcs[i].videoElement.load();
							});
						}
						//continue;  // currently disabling this, since we want to show it.
					} else if (session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted || session.rpcs[i].directorVideoMuted) {
						continue;
					}

					if (session.scene !== false) {
						if (session.sceneType === 3) {
							// order
							countOrder += 1;
							if (session.order === false) {
								if (countOrder == 1) {
									session.rpcs[i].videoElement.style.display = "block";
								} else {
									session.rpcs[i].videoElement.style.display = "none";
								}
							} else if (session.order === countOrder) {
								session.rpcs[i].videoElement.style.display = "block";
							} else {
								session.rpcs[i].videoElement.style.display = "none";
							}
						}
					}

					if (session.rpcs[i].videoElement.style.display == "none") {
						// Video is disabled; run at lowest
						if (session.scene !== false) {
							delayedRequestRate(session.hiddenSceneViewBitrate, i, true); // hidden. I dont want it to be super low, for video quality reasons.
							if (!session.hiddenSceneViewBitrate) {
								session.rpcs[i].videoElement.nogb = 2;
							}
						} else {
							delayedRequestRate(0, i, true); // w/e   This is not in OBS, so we just set it as low as possible.  Shoudln't exist really unless loading?
						}
					} else if (session.scene !== false) {
						// max

						//

						if (sceneBitrate !== false) {
							if (screenShareBitrate !== false && session.rpcs[i].screenShareState) {
								delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality.
							} else {
								delayedRequestRate(sceneBitrate, i); // well, screw that. Setting it to room quality.
							}
						} else {
							if (screenShareBitrate !== false && session.rpcs[i].screenShareState) {
								delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality.
							} else {
								delayedRequestRate(-1, i); // unlock.
							}
						}
						if (session.rpcs[i].order !== false) {
							session.rpcs[i].videoElement.order = session.rpcs[i].order;
						} else {
							session.rpcs[i].videoElement.order = 0;
						}
						if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
							if (!(session.rpcs[i].videoElement in mediaPool_invisible)) {
								//	mediaPool_invisible.push(session.rpcs[i].videoElement);
							} else {
								errorlog("THIS SHOULD NOT HAPPEN; 650");
							}
						} else if (!doNotPush) {
							mediaPool.push(session.rpcs[i].videoElement);
						}
					} else if (session.roomid !== false) {
						// guests should see video at low bitrate, ie: 100kbps (not 35kbps like if disabled)
						if (session.rpcs[i].order !== false) {
							session.rpcs[i].videoElement.order = session.rpcs[i].order;
						} else {
							session.rpcs[i].videoElement.order = 0;
						}
						if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
							if (!(session.rpcs[i].videoElement in mediaPool_invisible)) {
								//		mediaPool_invisible.push(session.rpcs[i].videoElement);
							} else {
								errorlog("THIS SHOULD NOT HAPPEN; 665");
							}
						} else if (!doNotPush) {
							mediaPool.push(session.rpcs[i].videoElement);
						}
						if (session.roomid === "" && session.bitrate) {
							// we will let the URL specified bitrate hold, since this isn't a real room.
							delayedRequestRate(-1, i);
						} else {
							if (screenShareBitrate !== false && session.rpcs[i].screenShareState) {
								delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality.
							} else {
								delayedRequestRate(roomBitrate, i); // well, screw that. Setting it to room quality.
							}
						}
					} else {
						// view=xx,yy  or whatever.  This should be highest quality.
						if (session.rpcs[i].order !== false) {
							session.rpcs[i].videoElement.order = session.rpcs[i].order;
						} else {
							session.rpcs[i].videoElement.order = 0;
						}
						if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
							if (!(session.rpcs[i].videoElement in mediaPool_invisible)) {
								//		mediaPool_invisible.push(session.rpcs[i].videoElement);
							} else {
								errorlog("THIS SHOULD NOT HAPPEN; 684");
							}
						} else if (!doNotPush) {
							mediaPool.push(session.rpcs[i].videoElement);
						}
						if (sceneBitrate) {
							delayedRequestRate(sceneBitrate, i);
						} else if (session.screenShareBitrate !== false && session.rpcs[i].screenShareState) {
							// session.screenShareBitrate is non-room
							delayedRequestRate(session.screenShareBitrate, i); // well, screw that. Setting it to room quality.
						} else {
							delayedRequestRate(-1, i);
						}
					}
					if (session.rpcs[i].videoElement.nogb == 2) {
						session.rpcs[i].videoElement.nogb = 1;
						session.rpcs[i].videoElement.classList.add("nogb");
					} else if (session.rpcs[i].videoElement.nogb == 1) {
						session.rpcs[i].videoElement.nogb = 0;
						session.rpcs[i].videoElement.classList.remove("nogb");
					}
				}
			}
		}

		if (session.broadcastIFrame && session.broadcastIFrame.src) {
			// keep alive iframes whennot visible. i think
			if (!mediaPool.length) {
				mediaPool.push(session.broadcastIFrame);
			}
		}

		if (document.fullscreenElement) {
			try {
				if (document.fullscreenElement.tagName === "VIDEO") {
					// if its HTML, than we assume its the full canvas
					for (var i = 0; i < mediaPool.length; i++) {
						// if its your local camera, it shouldn't be a problem, so we can focus on remote cameras only
						if (mediaPool[i].id !== document.fullscreenElement.id) {
							// if its selected camera, we want to exclude it
							if (mediaPool[i].dataset && mediaPool[i].dataset.UUID && mediaPool[i].tagName && mediaPool[i].tagName == "VIDEO") {
								delayedRequestRate(session.hiddenSceneViewBitrate, mediaPool[i].dataset.UUID, null); // null implies don't change the current audio setting
								mediaPool_invisible.push(mediaPool[i]); // move visible elements to the invisible list, since something is full screen
								mediaPool.splice(i, 1);
							}
						}
					}
				}
			} catch (e) {
				errorlog(e);
			}
		}

		var sscount = 0;

		var skip = false;

		for (var m = 0; m < mediaPool.length; m++) {
			mediaPool[m].alreadyAdded = false;
		}

		if (!session.layout || session.activeSpeaker) {
			if (session.orderby) {
				if (session.orderby == "id") {
					mediaPool.sort(compare_vids_sid);
				} else if (session.orderby == "label") {
					mediaPool.sort(compare_vids_label);
				} else {
					mediaPool.sort(compare_vids_sid);
				}
			}
			mediaPool.sort(compare_vids);
		} else if (session.exclusiveLayoutAudio) {
			[...mediaPool].forEach(ele => {
				if (ele.dataset.sid) {
					if (session.layout[ele.dataset.sid]) {
						return;
					} else if (session.layout[""]) {
						let matched = false;
						session.layout[""].forEach(i => {
							if (i.defaultStreamID && i.defaultStreamID == ele.dataset.sid) {
								matched = true;
							}
						});
						if (matched) {
							return;
						}
					}
					const index = mediaPool.indexOf(ele);
					if (index > -1) {
						if (ele.dataset.UUID && session.scene !== false) {
							delayedRequestRate(session.hiddenSceneViewBitrate, ele.dataset.UUID, false); // it's added already, so we know it needs sound.  But lets d
						}

						mediaPool.splice(index, 1); // 2nd parameter means remove one item only
					}
				}
			});
			if (RPCSkeys) {
				for (var keyIndex = 0; keyIndex < RPCSkeys.length; keyIndex++) {
					i = RPCSkeys[keyIndex];
					if (session.rpcs[i] === null) {
						continue;
					}
					if (session.rpcs[i].streamID) {
						let matched = false;
						mediaPool.forEach(ele => {
							if (ele.dataset.sid == session.rpcs[i].streamID) {
								matched = true;
							}
						});
						if (!matched) {
							if (session.rpcs[i].mutedStateMixer === false) {
								session.rpcs[i].mutedStateMixer = true;
								applyMuteState(i);
							}
						}
					}
				}
			}
		}

		if (session.fakeFeeds && session.fakeFeeds.length && mediaPool.length < session.fakeFeeds.length) {
			for (let i = 0; i < session.fakeFeeds.length; i++) {
				if (mediaPool.length < session.fakeFeeds.length) {
					mediaPool.push(session.fakeFeeds[i]);
				} else {
					try {
						session.fakeFeeds[i].remove();
					} catch (e) {
						errorlog(e);
					}
				}
			}
		}
		
		if (session.slotsList && session.slotsList.length > 0) {
			// Filter mediaPool to only include videos in the slotsList
			const filteredMediaPool = [];
			for (let i = 0; i < mediaPool.length; i++) {
				if (session.slotsList.includes(i + 1)) { // +1 for 1-indexed slotsList
					filteredMediaPool.push(mediaPool[i]);
				}
			}
			mediaPool = filteredMediaPool;
			
		}

		var mpl = session.slots || mediaPool.length;
		

		if (!sssid) {
			if (mpl > 1) {
				var BB = 0;
				var rw = 1;
				var rh = 1;
				var NW;
				var NH;
				var current;
				for (NW = 1; NW <= mpl; NW++) {
					NH = Math.ceil(mpl / NW);
					var www = ww / NW;
					var hhh = hh / NH;
					if (www > hhh) {
						current = Math.round(hhh * hhh * (mpl / (NW * NH)));
					} else {
						current = Math.round(www * www * (mpl / (NW * NH)));
					}

					if (current >= BB) {
						BB = current;
						rw = NW;
						rh = NH;
					}

					if (mediaPool[NW - 1]) {
						//if (mediaPool[NW-1].tagName == "VIDEO"){
						if (mediaPool[NW - 1].dataset.UUID) {
							if (mediaPool[NW - 1].dataset.UUID in session.rpcs) {
								if (session.rpcs[mediaPool[NW - 1].dataset.UUID].screenShareState && !session.rpcs[mediaPool[NW - 1].dataset.UUID].smallScreen) {
									sscount += 1;
									sssid = mediaPool[NW - 1].dataset.sid;
								}
							}
						} else if ("id" in mediaPool[NW - 1] && mediaPool[NW - 1].id == "screensharesource" && session.notifyScreenShare) {
							sscount += 1;
							sssid = mediaPool[NW - 1].dataset.sid;
						}
					}
				}
			} else {
				var rw = 1;
				var rh = 1;
			}

			if (sscount > 1) {
				sssid = false; // lets not maximize if more than one screen share.
			}
		}
	} catch (e) {
		errorlog(e);
		sssid = false;
	}

	// Add screen share status classes to the gridlayout element
	if (sscount > 0) {
		playarea.classList.add("has-screenshare");
		playarea.classList.remove("no-screenshare");
	} else {
		playarea.classList.add("no-screenshare");
		playarea.classList.remove("has-screenshare");
	}

	var customLayout = false;

	if (!session.notifyScreenShare && session.scene !== false) {
		// this is a scene, so lets assume &smallshare will disable larger screen shares since there is no one to screen share.
	} else if (sssid && session.screenshareStyle && (session.screenshareStyle==1) && (!session.layout || session.activeSpeaker)) {
		customLayout = {};
		if (mediaPool.length >= 12) {
			customLayout[sssid] = { x: 10, y: 10, w: 90, h: 90, c: false };
		} else if (mediaPool.length >= 10) {
			customLayout[sssid] = { x: 0, y: 10, w: 100, h: 90, c: false };
		} else if (mediaPool.length >= 8) {
			customLayout[sssid] = { x: 20, y: 20, w: 80, h: 80, c: false };
		} else if (mediaPool.length == 7) {
			customLayout[sssid] = { x: 16.66667, y: 0, w: 83.33333, h: 100, c: false };
		} else if (mediaPool.length == 5 || mediaPool.length == 6) {
			customLayout[sssid] = { x: 20, y: 0, w: 80, h: 100, c: false };
		} else {
			customLayout[sssid] = { x: 20, y: 0, w: 80, h: 100, c: false };
		}

		var posCount = 0;
		for (var i = 0; i < mediaPool.length; i++) {
			if (mediaPool[i].dataset.sid === sssid) {
				continue;
			}
			if (mediaPool.length == 2) {
				customLayout[mediaPool[i].dataset.sid] = { x: 0, y: 0, w: 20, h: 100, c: session.cover };
			} else if (mediaPool.length == 3) {
				customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 41 + 9, w: 20, h: 41, c: session.cover };
			} else if (mediaPool.length == 4) {
				customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 33.3333, w: 20, h: 33.3333, c: session.cover };
			} else if (mediaPool.length == 5) {
				customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: session.cover };
			} else if (mediaPool.length == 6) {
				customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 20, w: 20, h: 20, c: session.cover };
			} else if (mediaPool.length == 7) {
				customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 16.66667, w: 16.66667, h: 16.66667, c: session.cover };
			} else if (mediaPool.length == 8) {
				if (posCount == 0 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 70, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 1 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 50, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 2 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 30, y: 0, w: 20, h: 20, c: true };
				} else {
					customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: true };
				}
			} else if (mediaPool.length == 9) {
				if (posCount == 0 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 80, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 1 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 60, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 2 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 40, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 3 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 20, y: 0, w: 20, h: 20, c: true };
				} else {
					customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: true };
				}
			} else if (mediaPool.length >= 10) {
				if (posCount < 10) {
					customLayout[mediaPool[i].dataset.sid] = { x: 90 - (10 * posCount), y: 0, w: 10, h: 10, c: true };
				} else {
					customLayout[mediaPool[i].dataset.sid] = { x: 0, y: (posCount - 9) * 10, w: 10, h: 10, c: true };
				}
			} else {
				customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 20, w: 20, h: 20, c: true };
			}
			posCount += 1;
		}
	} else if (sssid && (!session.layout || session.activeSpeaker)) {
		customLayout = {};

		if (mediaPool.length >= 12) {
			customLayout[sssid] = { x: 0, y: 10, w: 90, h: 90, c: false };
		} else if (mediaPool.length >= 10) {
			customLayout[sssid] = { x: 0, y: 10, w: 100, h: 90, c: false };
		} else if (mediaPool.length >= 8) {
			customLayout[sssid] = { x: 0, y: 20, w: 80, h: 80, c: false };
		} else if (mediaPool.length == 7) {
			customLayout[sssid] = { x: 0, y: 0, w: 83.33333, h: 100, c: false };
		} else if (mediaPool.length == 5) {
			customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false };
		} else if (mediaPool.length == 6) {
			customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false };
		} else {
			customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false };
		}
		var posCount = 0;
		for (var i = 0; i < mediaPool.length; i++) {
			if (mediaPool[i].dataset.sid === sssid) {
				continue;
			}
			if (mediaPool.length == 2) {
				//
				customLayout[mediaPool[i].dataset.sid] = { x: 80, y: 0, w: 20, h: 100, c: session.cover };
			} else if (mediaPool.length == 3) {
				//
				customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 41 + 9, w: 20, h: 41, c: session.cover };
			} else if (mediaPool.length == 4) {
				//
				customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 33.3333, w: 20, h: 33.3333, c: session.cover };
			} else if (mediaPool.length == 5) {
				customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: session.cover };
			} else if (mediaPool.length == 6) {
				customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 20, w: 20, h: 20, c: session.cover };
			} else if (mediaPool.length == 7) {
				customLayout[mediaPool[i].dataset.sid] = { x: 83.33333, y: posCount * 16.66667, w: 16.66667, h: 16.66667, c: session.cover };
			} else if (mediaPool.length == 8) {
				if (posCount == 0 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 10, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 1 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 30, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 2 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 50, y: 0, w: 20, h: 20, c: true };
				} else {
					customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: true };
				}
			} else if (mediaPool.length == 9) {
				if (posCount == 0 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 0, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 1 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 20, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 2 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 40, y: 0, w: 20, h: 20, c: true };
				} else if (posCount == 3 + 4) {
					customLayout[mediaPool[i].dataset.sid] = { x: 60, y: 0, w: 20, h: 20, c: true };
				} else {
					customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: true };
				}
			} else if (mediaPool.length >= 10) {
				if (posCount < 10) {
					customLayout[mediaPool[i].dataset.sid] = { x: 10 * posCount, y: 0, w: 10, h: 10, c: true };
				} else {
					customLayout[mediaPool[i].dataset.sid] = { x: 90, y: (posCount - 9) * 10, w: 10, h: 10, c: true };
				}
			} else {
				customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 20, w: 20, h: 20, c: true }; //
			}
			posCount += 1;
		}
	
	} else if (session.rows && mediaPool.length) {
		try {
			customLayout = {};
			let n = mediaPool.length;
			
			
			if (session.slots && n < session.slots) {
				n = session.slots; // If we have fewer videos than slots, still use the full slots count
			}
			
			let rows = 1;
			if (session.rows.length >= n) {
				rows = parseInt(session.rows[n - 1]) || 1;
			} else {
				rows = parseInt(session.rows[session.rows.length - 1]) || 1;
			}
			
			if (rows<0){
				rows = Math.abs(rows);
				let cols = Math.ceil(n / rows) || 1;
				for (var i = 0; i < n; i++) {
					let col = i % cols;
					let row = parseInt(i / cols) % rows;
					
					if (row === Math.floor((n-1)/cols) && n % cols !== 0) {  // Last row logic
						let itemsInLastRow = n % cols || cols;
						let offset = (cols - itemsInLastRow) * (100 / cols) / 2;
						customLayout[mediaPool[i].dataset.sid] = {
							y: (100 / rows) * row,
							h: 100 / rows,
							x: offset + (100 / cols) * col,
							w: 100 / cols,
							c: session.cover
						};
					} else {
						customLayout[mediaPool[i].dataset.sid] = {
							y: (100 / rows) * row,
							h: 100 / rows,
							x: (100 / cols) * col,
							w: 100 / cols,
							c: session.cover
						};
					}
				}
			} else {
			
				let cols;
				
				if (session.slots) {
					cols = session.slots / rows;
				} else {
					cols = Math.ceil(n / rows) || 1;
				}

				for (var i = 0; i < n; i++) {
					let col = i % cols;
					let row = parseInt(i / cols) % rows;
					//console.log(row,col, rows,cols,i,n)
					if (cols >= 2 && rows >= 2 && n < (row + 1) * cols) {
						let delta = (row + 1) * cols - n;
						customLayout[mediaPool[i].dataset.sid] = { y: (100 / rows) * row, h: 100 / rows, x: (100 / cols) * (col + delta), w: 100 / cols, c: session.cover };
					} else {
						customLayout[mediaPool[i].dataset.sid] = { y: (100 / rows) * row, h: 100 / rows, x: (100 / cols) * col, w: 100 / cols, c: session.cover };
					}
				}
			}
		} catch (e) {
			errorlog(e);
		}
	}

	try {
		if (!skip) {
			var childNodes = playarea.childNodes;

			for (var n = 0; n < childNodes.length; n++) {
				if (childNodes[n].querySelector("video")) {
					var vidtemp = childNodes[n].querySelector("video");
					var matched = false;
					for (var m = 0; m < mediaPool.length; m++) {
						if (vidtemp.id === mediaPool[m].id) {
							vidtemp.alreadyAdded = true;
							mediaPool[m] = vidtemp;
							matched = true;
							childNodes[n].matched = true;
							break;
						}
					}
					if (!matched && vidtemp.isInvisible) {
						vidtemp.isInvisible = false;
						if (session.pauseInvisible && vidtemp.dataset.UUID){
							applyMuteState(vidtemp.dataset.UUID);
						}
					}
				} else if (childNodes[n].querySelector("iframe")) {
					var iftemp = childNodes[n].querySelector("iframe");
					if (iftemp.framesource) {
						if ((!session.activeSpeaker && session.layout) && session.layout[""]) {
							for (var i = 0; i < session.layout[""].length; i++) {
								if (session.layout[""][i].iframeSrc && session.layout[""][i].iframeSrc == iftemp.framesource) {
									childNodes[n].matched = true;
									if (iftemp.isConnected) {
										iftemp.alreadyAdded = true;
									}
								}
							}
						}
						continue;
					}
					for (var m = 0; m < mediaPool.length; m++) {
						if (mediaPool[m].nodeName === "IFRAME" && mediaPool[m].src && iftemp.src === mediaPool[m].src) {
							iftemp.alreadyAdded = true;
							iftemp.id = mediaPool[m].id;
							if (session.directorList.indexOf(iftemp.dataset.UUID) == -1) {
								iftemp.dataset.UUID = mediaPool[m].dataset.UUID;
								iftemp.dataset.sid = mediaPool[m].dataset.sid;
							}
							mediaPool[m] = iftemp;
							childNodes[n].matched = true;
							break;
						}
					}
					for (var m = 0; m < mediaPool_invisible.length; m++) {
						if (mediaPool_invisible[m].nodeName === "IFRAME" && mediaPool_invisible[m].src && iftemp.src === mediaPool_invisible[m].src) {
							iftemp.alreadyAdded = true;
							iftemp.id = mediaPool_invisible[m].id;
							if (session.directorList.indexOf(iftemp.dataset.UUID) == -1) {
								iftemp.dataset.UUID = mediaPool_invisible[m].dataset.UUID;
								iftemp.dataset.sid = mediaPool_invisible[m].dataset.sid;
							}
							mediaPool_invisible[m] = iftemp;
							childNodes[n].matched = true;
							break;
						}
					}
				}
			}

			for (var n = 0; n < childNodes.length; n++) {
				if (!childNodes[n].matched) {
					playarea.removeChild(childNodes[n]);
					n--;
				} else {
					childNodes[n].matched = null;
				}
			}
		}
	} catch (e) {
		errorlog(e);
	}

	if (session.videoElement && (session.videoElement.src || session.videoElement.srcObject)) {
		// fileshare or stream
		if ("playlist" in session.videoElement) {
			playarea.appendChild(session.videoElement); // fileshare.
		} else if (session.videoElement.style.display !== "none") {
			if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length) {
				if (miniPreview) {
					var container = null;
					if (mpl === 0 && miniPreview === 2) {
						if (soloVideo !== true) {
							//  since miniPreview==2, we want to full screen our preview.  Deleting the old mini preview container will ensure it loads right.
							if (!session.layout || session.activeSpeaker) {
								if (session.videoElement.container && session.videoElement.container.id == "minipreview") {
									delete session.videoElement.container;
								}
								if (document.getElementById("minipreview")) {
									document.getElementById("minipreview").remove();
								}
							}
							//

							mediaPool.push(session.videoElement);

							mpl = 1;
						}
					} else if (miniPreview === 3) {
						if (soloVideo !== true) {
							container = document.createElement("div");
							session.videoElement.container = container;
							container.style.top = "-500px";
							container.style.left = "-500px";
							container.style.width = "1px";
							container.style.height = "1px";
							//container.style.display = "flex";
							container.style.zIndex = "0";
							container.style.margin = "0";
							container.style.position = "absolute";
							container.style.cursor = "pointer";
							container.style.border = "0";
							container.appendChild(session.videoElement);
							playarea.appendChild(container);

						}
					} else if (soloVideo !== true) {
						if (document.getElementById("minipreview")) {
							container = document.getElementById("minipreview");
						} else {
							container = document.createElement("div");
							var togglePreview = document.createElement("div");

							togglePreview.className = "togglePreview";
							try {
								container.style.top = "calc(" + hi + "px + 2vh)";
								container.style.maxHeight = parseInt(playarea.offsetHeight) + "px";
								togglePreview.style.top = "calc(" + hi + "px + 2vh)";
								togglePreview.style.maxHeight = parseInt(playarea.offsetHeight) + "px";
							} catch (e) {
								errorlog(e);
								container.style.top = hi + "px";
								togglePreview.style.top = hi + "px";
							}
							//
							if (miniPerformerY !== null) {
								container.style.top = miniPerformerY + "%";
							}
							if (miniPerformerX !== null) {
								if (session.widget && !session.leftMiniPreview) {
									if (miniPerformerX > (99-session.widgetwidth)) {
										miniPerformerX = 99-session.widgetwidth;
									}
								}
								container.style.left = miniPerformerX + "%";
							} else if (session.leftMiniPreview !== false) {
								container.style.left = session.leftMiniPreview + "%";
								togglePreview.style.left = session.leftMiniPreview + "%";
							} else if (session.widget) {
								container.style.right = session.widgetwidth+"%";
								togglePreview.style.right = session.widgetwidth+"%";
							} else {
								container.style.right = "2vw";
								togglePreview.style.right = "2vw";
							}

							container.appendChild(session.videoElement);
							session.videoElement.container = container;
							playarea.appendChild(container);
							togglePreview.innerHTML = '<i class="las la la-eye-slash"></i><i class="las la la-eye"></i>';

							if (!session.previewToggleState) {
								container.classList.toggle("hidden");
								togglePreview.classList.toggle("blinded");
							}
							if (!(iOS || iPad)) {
								playarea.appendChild(togglePreview);
							}
							togglePreview.onclick = function (event) {
								event.preventDefault();
								event.stopPropagation();
								getById("minipreview").classList.toggle("hidden");
								this.classList.toggle("blinded");
								session.previewToggleState = !session.previewToggleState;
								return false;
							};

							makeMiniDraggableElement(container);
							container.id = "minipreview";
						}

						container.style.width = "18%";
						//container.style.display = "flex";
						container.style.zIndex = "3";
						container.style.margin = "0";
						container.style.position = "absolute";
						container.style.cursor = "pointer";
						container.style.border = "2px #BBB solid";
						container.style.height = "block";

						applyMirror(session.mirrorExclude);
					} else if (soloVideo === true) {
						if (document.getElementById("minipreview")) {
							container = document.getElementById("minipreview");
							container.style.height = "100%";
							//container.style.transform = "block";
							//container.style.transformOrigin = "unset";
						}
					}
					if (session.ruleOfThirds) {
						if (container && container.id == "minipreview" && !container.svg) {
							var svg = document.createElement("img");
							svg.src = session.ruleOfThirds;
							svg.style.width = "100%";
							svg.style.height = "100%";
							svg.style.position = "absolute";
							svg.style.left = "0";
							svg.style.top = "0";
							container.svg = svg;
							container.appendChild(svg);
						}
					}
					container = null; // clear reference
				}
			} else if (session.streamSrc && !session.videoElement.srcObject) {
				warnlog("THIS SHOULD NOT HAPPEN; 2067");
			}
		}
	}

	try {
		if (session.slots) {
			var slotArray = [];
			mediaPool.forEach(vid => {
				if (vid.slotBlank) {
					vid.slotBlank = false;
					vid.slot = 0;
				}

				if ("slot" in vid && vid.slot) {
					if (!slotArray.includes(parseInt(vid.slot))) {
						slotArray.push(parseInt(vid.slot));
					} else {
						vid.slot = 0;
						//mediaPool_invisible.push(vid);
						//var index = mediaPool.indexOf(vid);
						//if (index > -1) {
						//    mediaPool.splice(index, 1);
						//}
					}
				}
			});
			var slotCounter = 1;
			mediaPool.reverse();
			var j = mediaPool.length;
			while (j--) {
				if (!("slot" in mediaPool[j]) || mediaPool[j].slot == "0" || !mediaPool[j].slot) {
					while (slotArray.includes(slotCounter)) {
						slotCounter += 1;
					}
					slotArray.push(slotCounter);
					mediaPool[j].slot = slotCounter;
					mediaPool[j].slotBlank = true;
				}
			    if (!("slot" in mediaPool[j]) || !parseInt(mediaPool[j].slot) || mediaPool[j].slot == "0" || !mediaPool[j].slot || session.slots < parseInt(mediaPool[j].slot)) {
				//if ((!("slot" in mediaPool[j]) || !parseInt(mediaPool[j].slot) || mediaPool[j].slot == "0" || !mediaPool[j].slot || session.slots < parseInt(mediaPool[j].slot)) && !customLayout) {
					mediaPool_invisible.push(mediaPool[j]);
					mediaPool.splice(j, 1);
				}
			}
			mediaPool.reverse();
		}
		
		if (session.pauseInvisible){ 
			mediaPool_invisible.forEach(vid => {  // This is an experimental version, with improvements? Maybe its better?
				if (vid) {
					try {
						vid.style.width = "";
						vid.style.height = "";
						if (!vid.isInvisible){
							vid.isInvisible = true;
							if (session.pauseInvisible && vid.dataset.UUID){
								session.requestRateLimit(session.hiddenSceneViewBitrate, vid.dataset.UUID, true);
								vid.muted = true;
							}
						}
						if (vid.dataset.doNotMove) {
							return;
						}
						vid.style.top = "0px";
						vid.style.left = "0px";
						if (vid.isConnected){
							console.warn("Video is invisible, yet connected to the DOM?");
						}
					} catch (e) {
						errorlog(e);
					}
				}
			});
		} else { // this is the old version; a fail safe.
			mediaPool_invisible.forEach(vid => {
				if (vid) {
					try {
						vid.style.width = "0px";
						vid.style.height = "0px";
						vid.style.top = "0px";
						vid.style.left = "0px";
						vid.isInvisible = true;
						if (vid.alreadyAdded && vid.alreadyAdded == true) {
							vid.alreadyAdded = false;
							return;
						} else if (vid.dataset.doNotMove) {
							return;
						}
						if (!(vid.nodeName=="IFRAME" && vid.isConnected)){
							playarea.appendChild(vid);
						}
					} catch (e) {
						errorlog(e);
					}
				}
			});
		}
		
	} catch (e) {
		errorlog(e);
	}

	var layout = false;
	if (customLayout || (!session.activeSpeaker && session.layout)) {
		layout = session.layout || customLayout;
		layout = { ...layout };
		if (layout[""]) {
			for (var i = 0; i < layout[""].length; i++) {
				if (layout[""][i].defaultStreamID && !layout[layout[""][i].defaultStreamID]) {
					var found = false;
					for (var ell in mediaPool) {
						if (mediaPool[ell].dataset.sid && mediaPool[ell].dataset.sid === layout[""][i].defaultStreamID) {
							layout[layout[""][i].defaultStreamID] = layout[""][i];
							found = true;
						}
					}
					if (found) {
						continue;
					}
				}
				layout["#" + i] = layout[""][i];
				if (layout["#" + i].iframeSrc) {
					if (session.iframeSrcs[layout["#" + i].iframeSrc]) {
						var ele = session.iframeSrcs[layout["#" + i].iframeSrc];
						ele.id = "#" + i;
					} else {
						var ele = loadIframe(parseURL4Iframe(layout["#" + i].iframeSrc), "#" + i);
						ele.framesource = layout["#" + i].iframeSrc;
						session.iframeSrcs[layout["#" + i].iframeSrc] = ele;
					}
					//ele.alreadyAdded = true;
					//ele.matched = true;
				} else if (layout["#" + i].backgroundMedia || layout["#" + i].text || layout["#" + i].foregroundMedia) {
					var ele = document.createElement("div");
					ele.dataset.sid = "#" + i;
				} else {
					continue;
				}
				ele.dataset.sid = "#" + i;
				mediaPool.push(ele);
			}
		}

		try {
			mediaPool = sortByZ(mediaPool, layout);
		} catch (e) {
			// layout = false;
			errorlog(e);
		}
	}
	var i = 0;
	var offset = 0;

	if (session.waitImage && !(mediaPool_invisible.length || mediaPool.length)) {
		if (!session.waitImageTimeoutObject) {
			session.waitImageTimeoutObject = setTimeout(function () {
				session.waitImageTimeoutObject = true;
				if (!document.getElementById("retryimage")) {
					playarea.innerHTML += '<img id="retryimage"/>';
					getById("retryimage").src = decodeURIComponent(session.waitImage);
					getById("retryimage").onerror = function () {
						this.style.display = "none";
					};

					if (session.cover) {
						getById("retryimage").style.objectFit = "cover";
					}
				}
				getById("retryimage").style.display = "block";
			}, session.waitImageTimeout);
		}
	} else if (session.waitImage) {
		try {
			clearTimeout(session.waitImageTimeoutObject);
			session.waitImageTimeoutObject = false;
			getById("retryimage").style.display = "none";
		} catch (e) {}
	}
	mediaPool.forEach(vid => {
		try {
			if (!vid || !("id" in vid)) {
				errorlog(vid);
				return;
			}

			if (vid.needsLoading) {
				try {
					vid.load();
				} catch (e) {
					errorlog(e);
				}
			}

			if (session.slots) {
				if ("slot" in vid && parseInt(vid.slot)) {
					i = parseInt(vid.slot) - 1;
					if (i < 0) {
						return;
					}
				} else {
					return;
				}
			}

			var offsetx = 0;
			if (i !== 0) {
				if (Math.ceil((i + 0.01) / rw) == rh) {
					if (mpl % rw) {
						offsetx = Math.max(((rw - (mpl % rw)) * (w / rw)) / 2, 0);
					}
				}
			}

			var cover = session.cover;
			var borderOffset = session.border || 0;
			var videoMargin = session.videoMargin || 0;
			var borderRadius = session.borderRadius || 0;
			var borderColor = session.borderColor || "#000";
			var fadein = session.fadein || false;
			var backgroundMedia = session.defaultMedia || false;
			var foregroundMedia = session.defaultOverlayMedia || false;
			var animated = session.animatedMoves || 0;
			var textOverlay = false;
			if (!borderOffset) {
				borderColor = "#0000";
			}

			if (layout) {
				if (!(vid.dataset.sid && vid.dataset.sid in layout)) {
					
					if (vid.container) {
						vid.container.style.display = "none";
					}
					
					vid.isInvisible = true;
					if (session.pauseInvisible && vid.dataset.UUID){
						session.requestRateLimit(session.hiddenSceneViewBitrate, vid.dataset.UUID, true);
						vid.muted = true;
						return;
					}
					
					if (vid.dataset.UUID) {
						delayedRequestRate(session.hiddenSceneViewBitrate, vid.dataset.UUID, false); // it's added already, so we know it needs sound.  But lets d
					}
					return;
				}
				if ("borderThickness" in layout[vid.dataset.sid]) {
					borderOffset = layout[vid.dataset.sid].borderThickness || 0;
				}
				if ("animated" in layout[vid.dataset.sid]) {
					animated = layout[vid.dataset.sid].animated || 0;
					if (animated === true) {
						animated = session.animatedMoves || 50;
					}
				}
				if ("margin" in layout[vid.dataset.sid]) {
					videoMargin = layout[vid.dataset.sid].margin || 0;
				}
				if ("rounded" in layout[vid.dataset.sid]) {
					borderRadius = layout[vid.dataset.sid].rounded || 0;
				}
				if (layout[vid.dataset.sid].borderColor) {
					borderColor = layout[vid.dataset.sid].borderColor;
				}
				if (layout[vid.dataset.sid].fadeIn) {
					fadein = layout[vid.dataset.sid].fadeIn;
				}
				if ("backgroundMedia" in layout[vid.dataset.sid]) {
					backgroundMedia = layout[vid.dataset.sid].backgroundMedia || false;
				}
				if ("foregroundMedia" in layout[vid.dataset.sid]) {
					foregroundMedia = layout[vid.dataset.sid].foregroundMedia || false;
				}
				
				if (layout[vid.dataset.sid].text) {
					
					if (!vid.container || !vid.container.textOverlay) {
						textOverlay = document.createElement("div");
						textOverlay.className = "textOverlay";
						//vid.container.appendChild(vid.container.textOverlay);
					} else {
						textOverlay = vid.container.textOverlay;
					}
					
					textOverlay.innerText = layout[vid.dataset.sid].text;
					textOverlay.style.color = layout[vid.dataset.sid].textColor || "#ffffff";
					textOverlay.style.fontSize = layout[vid.dataset.sid].fontSize || "24px";
					textOverlay.style.fontFamily = layout[vid.dataset.sid].fontFamily || "Arial, sans-serif";
					textOverlay.style.position = "absolute";
					textOverlay.style.width = "100%";
					textOverlay.style.textAlign = "center";
					textOverlay.style.zIndex = "10";
					
					// Position the text
					const textPosition = layout[vid.dataset.sid].textPosition || "50%";
					textOverlay.style.top = textPosition;
					textOverlay.style.transform = "translateY(-50%)";
					
					// Add background if specified
					if (layout[vid.dataset.sid].textBackground) {
						textOverlay.style.backgroundColor = layout[vid.dataset.sid].textBackground;
						textOverlay.style.padding = "10px";
					} else {
						textOverlay.style.backgroundColor = "transparent";
						textOverlay.style.textShadow = "1px 1px 2px rgba(0,0,0,0.8)";
					}
				} else if (vid.container && vid.container.textOverlay) {
					vid.container.textOverlay.remove();
					delete vid.container.textOverlay;
				}
				
				if (vid.container) {
					if (!(vid.nodeName == "IFRAME" && vid.isConnected)) {
						// moving an iframe will break it.
						if (!vid.alreadyAdded || vid.nodeName == "IFRAME") {
							playarea.appendChild(vid.container);
						}
					}
				}
			}

			var skipAnimation = false;
			if (vid.isInvisible) {
				vid.isInvisible = false;
				if (session.pauseInvisible && vid.dataset.UUID){ 
					applyMuteState(vid.dataset.UUID); 
				}
				skipAnimation = true;
				if (fadein) {
					vid.classList.add("fadein");
					if (vid.holder) {
						vid.holder.classList.add("fadein");
					}
				}
				
			}

			offsety = Math.max((h - Math.ceil(mpl / rw) * Math.ceil(h / rh)) / 2, 0);

			if (vid.container) {
				var container = vid.container;
				if (container.move) {
					clearInterval(container.move);
					container.move = null;
				}
			} else {
				var container = document.createElement("div");
				vid.container = container;
			}
			container.style.position = "absolute";
			container.style.display = "block";
			container.classList.add("container_holder_video");
			
			// Add screen share class to individual containers
			var isScreenShare = false;
			if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].screenShareState) {
				isScreenShare = true;
			} else if (vid.id === "screensharesource") {
				isScreenShare = true;
			}
			
			if (isScreenShare) {
				container.classList.add("is-screenshare");
				container.classList.remove("is-not-screenshare");
			} else {
				container.classList.add("is-not-screenshare");
				container.classList.remove("is-screenshare");
			}

			// ANIMATED  - CONTAINER ; width/height/z-index/cover///////////////
			if (layout) {
				try {
					var left = (w / 100) * layout[vid.dataset.sid].x || layout[vid.dataset.sid].xp || 0;
					var top = (h / 100) * layout[vid.dataset.sid].y || layout[vid.dataset.sid].yp || 0;
					top += hi;
					var width = (w / 100) * layout[vid.dataset.sid].w || layout[vid.dataset.sid].wp || 0;
					var height = (h / 100) * layout[vid.dataset.sid].h || layout[vid.dataset.sid].hp || 0;
				
					if (layout[vid.dataset.sid].cover || layout[vid.dataset.sid].c) {
						// this should be true/false
						//vid.style.objectFit = "cover";
						cover = layout[vid.dataset.sid].cover || layout[vid.dataset.sid].c;
					} else {
						//vid.style.objectFit = "contain"; // this should fall back to sessio.cover if no layout supplied
						cover = false;
					}
					//container.style.zindex = 0;
					container.style.zIndex = layout[vid.dataset.sid].zIndex || layout[vid.dataset.sid].z || 0;
				} catch(e){
					errorlog(e);
				}
			} else {
				var left = Math.max(offsetx + Math.floor((((i % rw) + 0) * w) / rw), 0);
				var top = Math.max(offsety + Math.floor(((Math.floor(i / rw) + 0) * h) / rh + hi), 0);
				var width = Math.ceil(w / rw);
				var height = Math.ceil(h / rh);
				//container.style.zIndex = 0;
			}

			var computed = getComputedStyle(vid);

			if (animated && !skipAnimation) {
				container.style.transition = "width " + animated + "ms ease-in-out 0s, height " + animated + "ms ease-in-out 0s, background-color " + animated + "ms ease-in-out 0s, transform " + animated + "ms ease-in-out 0s, top " + animated + "ms ease-in-out 0s, left " + animated + "ms ease-in-out 0s";
			} else {
				container.style.transition = "";
			}

			if (layout) {
				//////////////////  NOT ANIMATED - CONTAINER ; width/height/z-index/cover///////////////
				container.style.left = left + "px";
				container.style.top = top + "px";
				container.style.width = width + "px";
				container.style.height = height + "px";
				container.twidth = width;
				container.theight = height;
			} else {
				container.style.left = offsetx + Math.floor((((i % rw) + 0) * w) / rw) + "px";
				container.style.top = offsety + Math.floor(((Math.floor(i / rw) + 0) * h) / rh + hi) + "px";
				container.twidth = Math.ceil(w / rw);
				container.theight = Math.ceil(h / rh);
				container.style.width = container.twidth + "px";
				container.style.height = container.theight + "px";
			}

			var maxWidth = 0;
			if (parseInt(computed.width) > parseInt(container.style.width)) {
				maxWidth = computed.width;
			} else {
				maxWidth = container.style.width;
			}

			var maxHeight = 0;
			if (parseInt(computed.height) > parseInt(container.style.height)) {
				maxHeight = computed.height;
			} else {
				maxHeight = container.style.height;
			}
			if (cover === true) {
				vid.style.maxWidth = maxWidth;
				vid.style.maxHeight = maxHeight;
				vid.style.objectFit = "cover";
			} else if (cover == 2) {
				// For session.cover == 2, determine whether to use cover or contain
				// based on aspect ratio comparison
				vid.style.maxWidth = maxWidth;
				vid.style.maxHeight = maxHeight;
				
				const vw = vid.naturalWidth || vid.videoWidth || 0;
				const vh = vid.naturalHeight || vid.videoHeight || 0;
				
				if (vw && vh) {
					// Calculate aspect ratios
					const videoAspect = vw / vh;
					
					// Use container dimensions for comparison
					const containerWidth = parseFloat(maxWidth);
					const containerHeight = parseFloat(maxHeight);
					const containerAspect = containerWidth / containerHeight;
					
					// If video is wider than container proportionally (width being squished),
					// use cover. Otherwise use contain.
					if (videoAspect > containerAspect) {
						vid.style.objectFit = "cover";
					} else {
						vid.style.objectFit = "contain";
					}
				} else {
					// Default to contain if we can't determine dimensions
					vid.style.objectFit = "contain";
				}
			} else {
				vid.style.objectFit = "contain";
				vid.style.maxWidth = maxWidth;
				vid.style.maxHeight = maxHeight;
			}

			//try {
			if (vid.alreadyAdded && vid.alreadyAdded == true) {
				if (!container.holder) {
					var holder = document.createElement("div");
					container.holder = holder;
					holder.className = "holder";
					holder.dataset.holder = true;
					container.appendChild(holder);
					holder.appendChild(vid);
				} else {
					var holder = container.holder;
				}
			} else if (vid.dataset.doNotMove) {
				vid.style.position = "absolute";
				vid.style.left = left + "px";
				vid.style.top = top + "px";
				vid.style.width = width + "px";
				vid.style.height = height + "px";
				vid.style.display = "flex";
				i += 1;
				return;
			} else {
				if (!container.holder) {
					var holder = document.createElement("div");
					container.holder = holder;
					holder.className = "holder";
					holder.dataset.holder = true;
					holder.appendChild(vid);
					container.appendChild(holder);
				} else {
					var holder = container.holder;
					holder.prepend(vid);
				}
				playarea.appendChild(container);
				vid.style.maxWidth = "100%";
				vid.style.maxHeight = "100%";
			}

			if (layout) {
				var wrw = (w / 100) * layout[vid.dataset.sid].w || 0;
				var hrh = (h / 100) * layout[vid.dataset.sid].h || 0;
			} else {
				var wrw = w / rw;
				var hrh = h / rh;
			}

			if (backgroundMedia) {
				container.style.backgroundImage = "url(" + backgroundMedia + ")";
				if (cover) {
					container.style.backgroundSize = "cover";
				} else {
					container.style.backgroundSize = "contain";
				}
				container.style.backgroundPosition = "center";
				container.style.backgroundRepeat = "no-repeat";
			} else if (container.style.backgroundImage) {
				container.style.backgroundImage = "unset";
			}

			if (foregroundMedia) {
				if (!container.foregroundMedia) {
					container.foregroundMedia = document.createElement("img");
					container.foregroundMedia.className = "foregroundMedia";
					container.appendChild(container.foregroundMedia);
				}
				container.foregroundMedia.src = foregroundMedia;
			} else if (container.foregroundMedia) {
				try {
					container.foregroundMedia.remove();
					delete container.foregroundMedia;
				} catch (e) {
					errorlog(e);
				}
			}
			
			if (textOverlay && !container.textOverlay){
				container.appendChild(textOverlay);
			}

			if ("rotated" in vid && vid.rotated !== false) {
				if (vid.rotated == 90) {
					vid.style.transform = "rotate(90deg)";
				} else if (vid.rotated == 270) {
					vid.style.transform = "rotate(270deg)";
				} else if (vid.rotated == 180) {
					vid.style.transform = "rotate(180deg)";
				} else if (!vid.rotated) {
					vid.style.transform = "rotate(0deg)";
				}
			}

			vid.style.width = "100%";
			vid.style.height = "100%";
			holder.style.position = "absolute";

			if (vid.classList.contains("paused")) {
				if (holder.paused) {
					holder.paused.className = "playButton";
				} else {
					var paused = document.createElement("span");
					paused.id = "paused_" + vid.dataset.UUID;
					paused.className = "playButton";
					paused.dataset.UUID = vid.dataset.UUID;
					paused.onclick = function () {
						unPauseVideo(vid);
					};
					holder.paused = paused;
					holder.appendChild(paused);
				}
			} else if (holder.paused) {
				holder.paused.className = "hidden";
			}

			var vw = vid.naturalWidth || vid.videoWidth; // naturalWidth is for images I guess
			var vh = vid.naturalHeight || vid.videoHeight;

			// log(vw + " : "+vh);

			if (cover && !session.structure) {
				//////
				if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) {
					holder.style.left = borderOffset + "px";
					holder.style.top = borderOffset + "px";
					holder.style.height = "calc(100% - " + videoMargin * 2 + "px)";
					holder.style.width = "calc(100% - " + videoMargin * 2 + "px)";

					vid.style.width = height - (borderOffset + videoMargin) * 2 + "px";
					vid.style.height = width - (borderOffset + videoMargin) * 2 + "px";
					vid.style.left = 0;
					vid.style.top = 0;
				} else {
					holder.style.left = videoMargin + "px";
					holder.style.top = videoMargin + "px";
					holder.style.height = "calc(100% - " + videoMargin * 2 + "px)";
					holder.style.width = "calc(100% - " + videoMargin * 2 + "px)";

					vid.style.width = "100%";
					vid.style.height = "100%";
					vid.style.left = 0;
					vid.style.top = 0;
				}

				////////// COVER VERSION
				if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) {
					// do not dynamically scale the screen share feed.
				} else if (session.dynamicScale) {
					if (vid.dataset.UUID) {
						let targetWidth = wrw;
						let targetHeight = hrh;

						targetWidth -= (borderOffset + videoMargin) * 2;
						targetHeight -= (borderOffset + videoMargin) * 2;

						if (targetWidth < 0) {
							targetWidth = 0;
						}
						if (targetHeight < 0) {
							targetHeight = 0;
						}

						if (session.devicePixelRatio) {
							session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
						} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) {
							session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover);
						} else {
							session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
						}
					}
				}

				vid.style.borderColor = borderColor;
				vid.style.borderWidth = borderOffset + "px";
				vid.style.borderRadius = borderRadius + "px";
				holder.style.borderColor = borderColor;
				holder.style.borderWidth = "0px";
				holder.style.borderRadius = borderRadius + "px";
			} else if ((vw && vh) || (vid.width && vid.height) || vid.dataset.aspectRatio) {
				if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) {
					if (vw && vh) {
						var vvw = parseInt(vh);
						var vvh = parseInt(vw);
					} else if (vid.width && vid.height) {
						var vvw = parseInt(vid.height);
						var vvh = parseInt(vid.width);
					} else {
						// video disabled; fall back to aspect Ratio
						var vvw = 1;
						var vvh = vid.dataset.aspectRatio;
					}

					vid.style.objectFit = "cover"; //contain;
					vid.style.overflow = "unset"; //contain;
					//vid.style.maxWidth = "unset";
					//vid.style.maxHeight = "unset";
				} else {
					if (vw && vh) {
						var vvw = parseInt(vw);
						var vvh = parseInt(vh);
					} else if (vid.width && vid.height) {
						var vvw = parseInt(vid.width);
						var vvh = parseInt(vid.height);
					} else {
						var vvw = vid.dataset.aspectRatio;
						var vvh = 1;
					}
				}

				var asw = (wrw - videoMargin * 2 - borderOffset * 2) / vvw; // (window.innerWidth/ N)  /  vid.videoHeight;
				var ash = (hrh - videoMargin * 2 - borderOffset * 2) / vvh;

				if (session.structure) {
					// wrw x hrh
					var arx = (wrw - videoMargin * 2 - borderOffset * 2) / (hrh - videoMargin * 2 - borderOffset * 2);
					var tarx = arW / arH;
					//var arW = 16.0;
					//var arH = 9.0;
					if (arx > tarx) {
						// width is too long
						var hsw = hrh * tarx - videoMargin * 2 * tarx - borderOffset * 2;
						var hsl = (wrw - hsw) / 2;
						var hst = videoMargin;
						var hsh = hrh - videoMargin * 2;
					} else {
						var hsh = (wrw - videoMargin * 2 + borderOffset * 2) / tarx;
						var hst = (hrh - hsh) / 2;
						var hsl = videoMargin;
						var hsw = wrw - videoMargin * 2;
					}
				} else if (asw > ash) {
					var hsh = hrh - videoMargin * 2;
					var hst = videoMargin;
					var hsw = (hsh - borderOffset * 2) * (vvw / vvh) + borderOffset * 2;
					var hsl = (wrw - hsw) / 2;
				} else {
					var hsw = wrw - videoMargin * 2;
					var hsl = videoMargin;
					var hsh = (hsw - borderOffset * 2) / (vvw / vvh) + borderOffset * 2;
					var hst = (hrh - hsh) / 2;
				}

				holder.style.left = Math.floor(hsl) + "px"; // this needs to be replaced with padding.  This means testing with rotation = 90
				holder.style.top = Math.floor(hst) + "px";
				holder.style.width = Math.ceil(hsw) + "px";
				holder.style.height = Math.ceil(hsh) + "px";
				//holder.style.padding = videoMargin + "px";

				holder.style.borderColor = borderColor;
				holder.style.borderWidth = borderOffset + "px";
				holder.style.borderRadius = borderRadius + "px";
				vid.style.borderWidth = "0px";

				if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) {
					vid.style.width = Math.ceil(wrw - borderOffset * 2) + "px";
					vid.style.height = Math.ceil(hsw - borderOffset * 2) + "px";
					vid.style.left = 0;

					vid.style.maxWidth = "100vh";
					vid.style.maxHeight = "100vw";

					if (ChromiumVersion && ChromiumVersion < 77) {
						if (!animated && parseInt(container.style.width) > parseInt(holder.style.height)) {
							vid.style.position = "relative";
							vid.style.objectFit = "contain"; //contain;
						} else if (animated && container.twidth && parseInt(container.twidth) > parseInt(holder.style.height)) {
							vid.style.position = "relative";
							vid.style.objectFit = "contain"; //contain;
						}
					} else {
						vid.style.position = "relative";
					}
				} else if (session.blurBackground !== false && vid.nodeName == "VIDEO" && !container.blurred && vid.srcObject && ((asw > 1 && ash > 1) || asw >= 1 || ash >= 1)) {
					vid.srcObject.getVideoTracks().forEach(trk => {
						if (!container.blurred) {
							container.blurred = document.createElement("video");
							container.blurred.controls = false;
							container.blurred.style = "z-index:-10000;position:absolute;left:0;width:100%;height:100%;top:0;object-fit:fill;-webkit-filter: blur(" + session.blurBackground + "px)";
							container.blurred.srcObject = createMediaStream();
							container.blurred.srcObject.addTrack(trk);
							container.blurred.play();
							holder.appendChild(container.blurred);
						} else {
							if (container.blurred.paused) {
								container.blurred.play();
							}
						}
					});
				} else if (session.blurBackground !== false && vid.nodeName == "VIDEO" && vid.srcObject && ((asw > 1 && ash > 1) || asw >= 1 || ash >= 1)) {
					if (container.blurred.paused) {
						container.blurred.play();
					}
					//container.blurred.style = "z-index:-10000;position:absolute;left:0;width:100%;height:100%;top:0;object-fit:fill;-webkit-filter: blur("+session.blurBackground+"px)";
				} else if (container.blurred) {
					try {
						container.blurred.remove();
					} catch (e) {}
					try {
						delete container.blurred;
					} catch (e) {}
				}

				////////// NON-COVER VERSION (based on holder)
				if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) {
					// do not dynamically scale the screen share feed.
				} else if (session.dynamicScale) {
					if (vid.dataset.UUID) {
						let targetWidth = wrw;
						let targetHeight = hrh;

						targetWidth -= (borderOffset + videoMargin) * 2;
						targetHeight -= (borderOffset + videoMargin) * 2;

						if (targetWidth < 0) {
							targetWidth = 0;
						}
						if (targetHeight < 0) {
							targetHeight = 0;
						}

						if (session.devicePixelRatio) {
							session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
						} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) {
							session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover);
						} else {
							session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
						}
					}
				}
			} else {
				holder.style.left = borderOffset + videoMargin + "px";
				holder.style.top = borderOffset + videoMargin + "px";
				holder.style.height = "calc(100% - " + (borderOffset + videoMargin * 2) + "px)";
				holder.style.width = "calc(100% - " + (borderOffset + videoMargin * 2) + "px)";

				//////////  UNKNOWN VERSION
				if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) {
					// do not dynamically scale the screen share feed.
				} else if (session.dynamicScale) {
					if (vid.dataset.UUID) {
						let targetWidth = wrw;
						let targetHeight = hrh;

						targetWidth -= (borderOffset + videoMargin) * 2;
						targetHeight -= (borderOffset + videoMargin) * 2;

						if (targetWidth < 0) {
							targetWidth = 0;
						}
						if (targetHeight < 0) {
							targetHeight = 0;
						}

						if (session.devicePixelRatio) {
							session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
						} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) {
							session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover);
						} else {
							session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
						}
					}
				}
				///////////////
				holder.style.borderColor = borderColor;
				holder.style.borderWidth = borderOffset + "px";
				holder.style.borderRadius = borderRadius + "px";
				vid.style.borderWidth = "0px";
			}

			if (session.colorVideosBackground) {
				vid.style.backgroundColor = session.colorVideosBackground;
			} else {
				vid.style.backgroundColor = "unset";
			}

			if ((vid.dataset.UUID && session.rpcs && session.rpcs[vid.dataset.UUID] && 
					(
					  ("label" in session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].label !== false && session.showlabels === true) ||
					  (session.showmeta === true && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].meta)
					)) 
				||
				((session.showlabels === true || session.showmeta === true) && 
					(((vid.id === "videosource") && session.label) || vid.labelText || session.meta)
				)) {

				if (animated && container.twidth && container.theight) {
					var vidwidth = container.twidth;
					var vidheight = container.theight;
				} else {
					var vidwidth = vid.offsetWidth;
					var vidheight = vid.offsetHeight;
				}

				var fontsize = (vidwidth + vidheight) * 0.03;
				if (vidwidth / 16 >= vidheight / 9) {
					var voar = vidwidth / 16 / (vidheight / 9);
				} else {
					var voar = vidheight / 9 / (vidwidth / 16);
				}
				voar = Math.pow(voar, 0.5);
				fontsize = fontsize / voar;

				if (holder.label) {
					var label = holder.label;
				} else {
					var label = document.createElement("span");
					holder.label = label;
					if (session.labelstyle) {
						label.className = "video-label " + session.labelstyle;
					} else {
						label.className = "video-label";
					}
					holder.appendChild(label);
				}

				if (fontsize) {
					if (session.labelsize) {
						fontsize = (fontsize * session.labelsize) / 100;
					}
					label.style.fontSize = parseInt(fontsize) + "px";
				}

				if (  (
						vid.dataset.UUID && 
						session.rpcs && 
						session.rpcs[vid.dataset.UUID] && 
						(
						  // Label check
						  (
							"label" in session.rpcs[vid.dataset.UUID] && 
							session.rpcs[vid.dataset.UUID].label !== false && 
							session.showlabels === true
						  ) 
						  || 
						  // Meta check with explicit undefined checks
						  (
							session.showmeta === true && 
							session.rpcs[vid.dataset.UUID] && 
							session.rpcs[vid.dataset.UUID].meta
						  )
						)
					  )
					  ||
					  (
						(session.showlabels === true || session.showmeta === true) && 
						(
						  ((vid.id === "videosource") && session.label) || 
						  vid.labelText || 
						  session.meta
						)
					  )
					) {

					let labelContent = [];
					let imageElements = [];

					if (session.showlabels === true) {
						if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].label) {
							session.rpcs[vid.dataset.UUID].label.split("\\n").forEach(label => {
								labelContent.push({type: 'text', value: label});
							});
						} else if ((vid.id === "videosource") && session.label || vid.labelText) {
							(vid.labelText || session.label).split("\\n").forEach(label => {
								labelContent.push({type: 'text', value: label});
							});
						}
					}

					if (session.showmeta === true) {
						let metaData = [];
						if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].meta) {
							metaData = Object.entries(session.rpcs[vid.dataset.UUID].meta);
						} else if (session.meta) {
							metaData = Object.entries(session.meta);
						}

						metaData.forEach(([key, value]) => {
							if (value && typeof value === 'object') {
								if (value.type && value.type == 'file') {
									// console.log(value);
									if (value.filetype){
										imageElements.push(value);
									}
								} else {
									labelContent.push({
										type: 'text', 
										value: value.value || JSON.stringify(value)
									});
								}
							} else if (value) {
								labelContent.push({type: 'text', value: value});
							}
						});
					}

					// Create or get label container
					if (!holder.labelContainer) {
						holder.labelContainer = document.createElement("div");
						holder.labelContainer.className = "video-label-container";
						holder.appendChild(holder.labelContainer);
					}

					// Handle text content
					if (labelContent.length > 0) {
						if (!holder.label) {
							holder.label = document.createElement("div");
							if (session.labelstyle) {
								holder.label.className = "video-label " + session.labelstyle;
							} else {
								holder.label.className = "video-label";
							}
							holder.labelContainer.appendChild(holder.label);
						}

						// Apply font size adjustments as before
						if (fontsize) { 
							if (session.labelsize) {
								fontsize = (fontsize * session.labelsize) / 100;
							}
							holder.label.style.fontSize = parseInt(fontsize) + "px";
						}

						holder.label.innerHTML = labelContent
							.map(item => `<span>${escapeHtml(String(item.value))}</span>`)
							.join("");
					}

					// Handle image content
					if (imageElements.length > 0) {
						if (!holder.imageContainer) {
							holder.imageContainer = document.createElement("div");
							holder.imageContainer.className = "video-image-container";
							holder.imageContainer.style.position = "absolute";
							holder.imageContainer.style.top = "10px";
							holder.imageContainer.style.right = "10px";
							holder.imageContainer.style.display = "flex";
							holder.imageContainer.style.alignItems = "flex-start";
							holder.imageContainer.style.justifyContent = "flex-end";
							holder.appendChild(holder.imageContainer);
						}
						
						holder.imageContainer.style.maxWidth = "min(20vw, calc("+holder.style.width+" * 0.25))";
						holder.imageContainer.style.maxHeight = "min(20vh, calc("+holder.style.height+" * 0.25))";
						holder.imageContainer.style.minWidth = "max(7vw, calc("+holder.style.width+" * 0.22), 30px)";
						holder.imageContainer.style.minHeight = "max(7vw, calc("+holder.style.height+" * 0.22), 30px)";
						
							
						// Clear existing images
						holder.imageContainer.innerHTML = '';

						// Add new images
						imageElements.forEach(meta => {
							if (meta.filetype.startsWith("image/")){
								const imgElement = document.createElement("img");
								imgElement.src = meta.value;
								imgElement.className = `meta-image ${meta.templateName || ''}`;
								imgElement.style.width = "auto"; // Fill container width
								imgElement.style.height = "auto"; // Fill container height
								imgElement.style.objectFit = "contain"; // Maintain aspect ratio
								imgElement.style.position = "absolute";
								imgElement.style.maxWidth = "100%";
								imgElement.style.maxHeight = "100%";
								holder.imageContainer.appendChild(imgElement);
							}
						});
					}
				}
			} else if (holder.label) {
				holder.label.remove();
				delete holder.label;
			}

			if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID]) {
				if (session.rpcs[vid.dataset.UUID].voiceMeter) {
					holder.appendChild(session.rpcs[vid.dataset.UUID].voiceMeter);
				}
				if (session.rpcs[vid.dataset.UUID].remoteMuteElement) {
					holder.appendChild(session.rpcs[vid.dataset.UUID].remoteMuteElement);
				}

				if (session.signalMeter) {
					if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].signalMeter) {
						session.rpcs[vid.dataset.UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true);
						session.rpcs[vid.dataset.UUID].signalMeter.classList.remove("hidden");
						session.rpcs[vid.dataset.UUID].signalMeter.id = "signalMeter_" + vid.dataset.UUID;
						session.rpcs[vid.dataset.UUID].signalMeter.dataset.level = 0;
						session.rpcs[vid.dataset.UUID].signalMeter.title = getTranslation("signal-meter");
						holder.appendChild(session.rpcs[vid.dataset.UUID].signalMeter);
						holder.signalMeter = session.rpcs[vid.dataset.UUID].signalMeter;
					} else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].signalMeter) {
						if (!holder.signalMeter) {
							holder.appendChild(session.rpcs[vid.dataset.UUID].signalMeter);
							holder.signalMeter = session.rpcs[vid.dataset.UUID].signalMeter;
						}
					}
				}

				if (session.batteryMeter) {
					if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].batteryMeter) {
						session.rpcs[vid.dataset.UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true);
						session.rpcs[vid.dataset.UUID].batteryMeter.classList.remove("hidden");
						session.rpcs[vid.dataset.UUID].batteryMeter.id = "batteryMeter_" + vid.dataset.UUID;
						session.rpcs[vid.dataset.UUID].batteryMeter.dataset.level = 0;
						session.rpcs[vid.dataset.UUID].batteryMeter.title = getTranslation("battery-meter");
						holder.appendChild(session.rpcs[vid.dataset.UUID].batteryMeter);
						holder.batteryMeter = session.rpcs[vid.dataset.UUID].batteryMeter;
					} else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].batteryMeter) {
						if (!holder.batteryMeter) {
							holder.appendChild(session.rpcs[vid.dataset.UUID].batteryMeter);
							holder.batteryMeter = session.rpcs[vid.dataset.UUID].batteryMeter;
						}
					}
				}

				if (session.volumeControl && session.rpcs[vid.dataset.UUID].videoElement && vid.tagName != "VIDEO") {
					if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].volumeControl) {
						session.rpcs[vid.dataset.UUID].volumeControl = getById("volumeControlTemplate").cloneNode(true);
						session.rpcs[vid.dataset.UUID].volumeControl.classList.remove("hidden");
						session.rpcs[vid.dataset.UUID].volumeControl.id = "volumeControl_" + vid.dataset.UUID;
						session.rpcs[vid.dataset.UUID].volumeControl.value = parseInt(session.rpcs[vid.dataset.UUID].videoElement.volume * 100);
						session.rpcs[vid.dataset.UUID].volumeControl.dataset.UUID = vid.dataset.UUID;
						session.rpcs[vid.dataset.UUID].volumeControl.title = getTranslation("volume-control");
						session.rpcs[vid.dataset.UUID].volumeControl.oninput = function () {
							if (this.dataset.UUID && session.rpcs[this.dataset.UUID] && session.rpcs[this.dataset.UUID].videoElement) {
								session.rpcs[this.dataset.UUID].videoElement.volume = parseFloat(this.value / 100);
							}
						};
						holder.appendChild(session.rpcs[vid.dataset.UUID].volumeControl);
						holder.volumeControl = session.rpcs[vid.dataset.UUID].volumeControl;
					} else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].volumeControl) {
						if (!holder.volumeControl && session.rpcs[vid.dataset.UUID].videoElement) {
							holder.appendChild(session.rpcs[vid.dataset.UUID].volumeControl);
							holder.volumeControl = session.rpcs[vid.dataset.UUID].volumeControl;
							session.rpcs[vid.dataset.UUID].volumeControl.value = parseInt(session.rpcs[vid.dataset.UUID].videoElement.volume * 100);
						}
					}
				}

				if (session.showConnections) {
					if (!session.rpcs[vid.dataset.UUID].connectionDetails) {
						createConnectionDetailsEle(vid.dataset.UUID);
					}
					holder.appendChild(session.rpcs[vid.dataset.UUID].connectionDetails);
				}
			}

			if (session.ruleOfThirds) {
				if (vid.id == "videosource") {
					if (!holder.svg) {
						var svg = document.createElement("img");
						svg.src = session.ruleOfThirds;
						svg.style.width = "100%";
						svg.style.height = "100%";
						svg.style.position = "absolute";
						svg.style.left = "0";
						svg.style.top = "0";
						svg.style.pointerEvents = "none";
						holder.svg = svg;
						holder.appendChild(svg);
					}
				}
			}

			try {
				if (!(session.cleanOutput && session.cleanish == false)) {
					if (session.firstPlayTriggered === false) {
						// don't play unless needed; might cause clicking or who knows what else.
						if (vid.tagName.toLowerCase() == "video") {
							// we don't want to try playing an Iframe or Canvas.
							if (vid.paused) {
								warnlog("VIDEO IS NOT PLAYING");
								var playPromise = vid.play();
								if (playPromise !== undefined) {
									playPromise
										.then(_ => {
											// playing
											//session.firstPlayTriggered=true; // global tracking. "user gesture obtained", so no longer needed if playing.
										})
										.catch(err => {
											var bigPlayButton = document.getElementById("bigPlayButton");
											if (bigPlayButton) {
												bigPlayButton.innerHTML = '<span class="playButton"></span>';
												bigPlayButton.style.display = "block";
											}
										});
								} else {
									session.firstPlayTriggered = true; // well, I don't know if it's playing, and so whatever. fail gracefully.
								}
							}
						}
					}
				}
			} catch (e) {
				errorlog(e);
				var bigPlayButton = document.getElementById("bigPlayButton");
				if (bigPlayButton) {
					bigPlayButton.parentNode.removeChild(bigPlayButton);
				}
			}

			if (vid.nodeName == "IFRAME") {
				// I need to add this back in at some point.
				i += 1;
				return;
			}

			if (!session.cleanOutput && !session.nocursor && !session.nofullwindowbutton) {
				if (session.roomid !== false && session.scene === false) {
					if (!(vid.id === "videosource" && miniPreview) && !session.infocusForceMode) {
						if (!holder.button) {
							var button = document.createElement("div");
							holder.button = button;
							holder.appendChild(button);
						} else {
							var button = holder.button;
						}

						button.id = "button_" + vid.id;
						button.dataset.button = true;
						if (soloVideo) {
							button.innerHTML = "<img src='./media/sd.svg' class='fullwindowButtonimg' aria-hidden='true' />";
							button.title = "Show all active videos togethers";
							button.style.visibility = "visible";
						} else if (mpl > 1 || session.fullscreenButton) {
							// with session.fullscreenButton we hide the actuall full screen button, so this replaces it
							button.innerHTML = "<img src='./media/hd.svg' class='fullwindowButtonimg' aria-hidden='true' />";
							button.title = "Enlarge video and increase its clarity";
							button.style.visibility = "visible";
						} else {
							button.style.visibility = "hidden";
						}
						button.classList.add("fullwindowButton");

						if (vid.id == "videosource") {
							button.onclick = function (event) {
								if (session.infocus === true) {
									session.infocus = false;
								} else {
									session.infocus = true;
								}
								setTimeout(() => updateMixer(), 10);

								if (session.fullscreenButton) {
									if (session.infocus) {
										fullscreenPageToggle(true);
									} else {
										fullscreenPageToggle(false);
									}
								}
							};
						} else {
							button.dataset.UUID = vid.dataset.UUID;
							button.onclick = function (event) {
								var target = event.currentTarget;
								if (session.infocus === target.dataset.UUID) {
									//target.childNodes[0].className = 'las la-arrows-alt';
									session.infocus = false;
								} else {
									//target.childNodes[0].className = 'las la-compress';
									session.infocus = target.dataset.UUID;
									//log("session:"+target.dataset.UUID);
								}
								if (session.fullscreenButton) {
									if (session.infocus) {
										fullscreenPageToggle(true);
									} else {
										fullscreenPageToggle(false);
									}
								}
								setTimeout(() => updateMixer(), 10);
							};
						}
						vid.onclick = function (event) {
							if (session.disableMouseEvents) {
								return;
							}
							button.style.display = "block";
							container.style.backgroundColor = "#4444";
							button.style.opacity = "100%";
						};
						button.onmouseenter = function (event) {
							if (session.disableMouseEvents) {
								return;
							}
							button.style.display = "block";
							container.style.backgroundColor = "#4444";
							setTimeout(
								function (button) {
									button.style.opacity = "100%";
								},
								1,
								button
							);
						};
						button.onmousemove = function (event) {
							if (session.disableMouseEvents) {
								return;
							}
							button.style.display = "block";
							container.style.backgroundColor = "#4444";
							button.style.opacity = "100%";
						};
						container.onmousemove = function (event) {
							if (session.disableMouseEvents) {
								return;
							}
							button.style.display = "block";
							container.style.backgroundColor = "#4444";
							button.style.opacity = "100%";
						};
						container.onmouseenter = function (event) {
							if (session.disableMouseEvents) {
								return;
							}
							button.style.display = "block";
							container.style.backgroundColor = "#4444";
							setTimeout(
								function (button) {
									button.style.opacity = "100%";
								},
								1,
								button
							);
						};
						container.onmouseleave = function (event) {
							if (session.disableMouseEvents) {
								return;
							}
							button.style.display = "none";
							container.style.backgroundColor = null;
							button.style.opacity = "10%";
						};
					} else if (vid.id === "videosource" && miniPreview && soloVideo == true && !session.infocusForceMode) {
						if (!holder.button) {
							var button = document.createElement("div");
							holder.button = button;
							holder.appendChild(button);
						} else {
							var button = holder.button;
						}

						button.id = "button_videosource";
						button.dataset.button = true;
						if (soloVideo) {
							button.innerHTML = "<img src='./media/sd.svg' class='fullwindowButtonimg' aria-hidden='true' />";
							button.title = "Show all active videos togethers";
							button.style.display = "unset";
						} else {
							button.style.visibility = "hidden";
							button.style.display = "none";
						}
						button.classList.add("fullwindowButton");

						button.onclick = function (event) {
							event.stopPropagation();
							event.preventDefault();

							if (session.infocus === true) {
								session.infocus = false;
								setTimeout(() => updateMixer(), 10);
							}

							if (session.fullscreenButton) {
								if (session.infocus) {
									fullscreenPageToggle(true);
								} else {
									fullscreenPageToggle(false);
								}
							}
						};
					} else if (session.infocusForceMode && holder.button) {
						try {
							holder.button.remove();
						} catch (e) {
							errorlog(e);
						}
					}
				}
			}
			i += 1;
		} catch (err) {
			errorlog(err);
		}
	});
	for (var uid in delayedRequestList) {
		session.requestRateLimit(...delayedRequestList[uid]);
	}
	updateUserList();
}

var translationBacklog = [];

function miniTranslate(ele, ident = false, direct = false) {
	if (!translation) {
		translation = {};
	}

	if (ident) {
		if (translation.innerHTML && ident in translation.innerHTML) {
			if (ele.querySelector("[data-translate]")) {
				ele.querySelector("[data-translate]").innerHTML = translation.innerHTML[ident];
				ele.querySelector("[data-translate]").dataset.translate = ident;
			} else {
				ele.innerHTML = translation.innerHTML[ident];
				ele.dataset.translate = ident;
			}
			return;
		} else if (direct) {
			if (ele.querySelector("[data-translate]")) {
				ele.querySelector("[data-translate]").innerHTML = direct;
				ele.querySelector("[data-translate]").dataset.translate = ident;
			} else {
				ele.dataset.translate = ident;
				ele.innerHTML = direct;
			}
			return;
		} else {
			if (!(ident in miscTranslations)) {
				var value = ident.replaceAll("-", " "); // lets use the key as the translation
			} else {
				var value = miscTranslations[ident]; // lets use a miscellaneous translation as backup?
			}

			if (ele.querySelector("[data-translate]")) {
				ele.querySelector("[data-translate]").innerHTML = value;
				ele.querySelector("[data-translate]").dataset.translate = ident;
			} else {
				ele.innerHTML = value;
				ele.dataset.translate = ident;
			}
			return;
		}
	}

	var allItems = ele.querySelectorAll("[data-translate]");
	allItems.forEach(function (ele2) {
		if (translation.innerHTML && ele2.dataset.translate in translation.innerHTML) {
			ele2.innerHTML = translation.innerHTML[ele2.dataset.translate];
		} else if (translation.miscellaneous && ele2.dataset.translate in translation.miscellaneous) {
			ele2.innerHTML = translation.miscellaneous[ele2.dataset.translate];
		}
	});
	if (ele.dataset) {
		if (translation.innerHTML && ele.dataset.translate in translation.innerHTML) {
			ele.innerHTML = translation.innerHTML[ele.dataset.translate];
		} else if (translation.miscellaneous && ele.dataset.translate in translation.miscellaneous) {
			ele.innerHTML = translation.miscellaneous[ele.dataset.translate];
		}
	}
	if (translation.titles) {
		var allTitles = ele.querySelectorAll("[title]");
		allTitles.forEach(function (ele2) {
			if (ele.dataset.key) {
				var key = ele2.dataset.key;
			} else {
				var key = ele2.title.replace(/[\W]+/g, "-").toLowerCase();
				ele2.dataset.key = key;
			}

			if (key in translation.titles) {
				ele2.title = translation.titles[key];
			} else if (ele2.dataset.translate && ele2.dataset.translate in translation.titles) {
				ele2.title = translation.titles[ele2.dataset.translate];
			}
		});
		if (ele.title) {
			if (ele.dataset.key) {
				var key = ele.dataset.key;
			} else {
				var key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
				ele.dataset.key = key;
			}

			if (key in translation.titles) {
				ele.title = translation.titles[key];
			} else if (ele.dataset.translate && ele.dataset.translate in translation.titles) {
				ele.title = translation.titles[ele.dataset.translate];
			}
		}
	}
	if (translation.placeholders) {
		var allPlaceholders = ele.querySelectorAll("[placeholder]");
		allPlaceholders.forEach(function (ele2) {
			var key = ele2.placeholder.replace(/[\W]+/g, "-").toLowerCase();
			if (key in translation.placeholders) {
				ele2.placeholder = translation.placeholders[key];
			}
		});

		if (ele.placeholder) {
			var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
			if (key in translation.placeholders) {
				ele.placeholder = translation.placeholders[key];
			}
		}
	}
}

async function changeLg(lang, rtl=false, save=false) {
	log("changeLg: " + lang);
	var retry = false;
	if (lang == "auto") {
		const navlang = navigator.language || navigator.userLanguage || "";
		const userLang = navlang.toLowerCase(); // Convert to lower case
		lang = userLang.replace("_", "-"); // Replace dash with underscore if present
		retry = true;
	}

	await fetchWithTimeout("./translations/" + lang + ".json", 2000)
		.then(async response => {
			try {
				if (response.status !== 200) {
					getById("mainmenu").style.opacity = 1;
					if (retry) {
						console.warn("Couldn't find the exact language file for '" + lang + "'; trying a more generic option instead");
						lang = lang.split("-")[0];
						if (lang && lang !== "auto") {
							await changeLg(lang, rtl); // Retry with a more generic language code.
						}
					}
					return;
				}
				await response
					.json()
					.then(async function (data) {
						translation = data; // translation.innerHTML[ele.dataset.translate]
						
						document.documentElement.dir = rtl ? "rtl" : "ltr";
						document.documentElement.setAttribute('lang', lang);
						
						document.body.classList.remove('rtl');
						document.body.classList.remove('ltr');
						
						document.body.classList.add(rtl ? 'rtl' : 'ltr');
						document.documentElement.style.setProperty('--rtl-or-ltr', rtl ? 'right' : 'left');
						
						if (save){
							try {
								localStorage.setItem("vdo_ninja_language", lang);
							} catch (e) {
								console.warn("Could not save language to localStorage", e);
							}
						}

						if (translation.miscellaneous) {
							Object.keys(translation.miscellaneous).forEach(key => {
								miscTranslations[key] = translation.miscellaneous[key];
							});
						}
						translation.miscellaneous = miscTranslations;

						var allItems = document.querySelectorAll("[data-translate]");
						allItems.forEach(function (ele) {
							if (ele.dataset.translate in translation.innerHTML) {
								ele.innerHTML = translation.innerHTML[ele.dataset.translate];
							} else if (translation.miscellaneous && ele.dataset.translate in translation.miscellaneous) {
								ele.innerHTML = translation.miscellaneous[ele.dataset.translate]; // use the misc translation if no main one is found
							}
						});

						var allTitles = document.querySelectorAll("[title]");
						allTitles.forEach(function (ele) {
							if (ele.dataset.key) {
								var key = ele.dataset.key;
							} else {
								var key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
								ele.dataset.key = key;
							}
							if (key in translation.titles) {
								ele.title = translation.titles[key];
							} else if (ele.dataset.translate && translation.titles && ele.dataset.translate in translation.titles) {
								ele.title = translation.titles[ele.dataset.translate];
							}
						});

						var allPlaceholders = document.querySelectorAll("[placeholder]");
						allPlaceholders.forEach(function (ele) {
							var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
							if (key in translation.placeholders) {
								ele.placeholder = translation.placeholders[key];
							}
						});

						if (translationBacklog.length) {
							for (var i = 0; i < translationBacklog.length; i++) {
								try {
									miniTranslate(translationBacklog[i][0], translationBacklog[i][1]);
								} catch (e) {}
							}
							translationBacklog = [];
						}

						getById("mainmenu").style.opacity = 1;
					})
					.catch(async err2 => {
						if (retry) {
							console.warn("Couldn't find the exact language file for '" + lang + "'; trying a more generic option instead");
							lang = lang.split("-")[0];
							if (lang && lang !== "auto") {
								await changeLg(lang, rtl); // won't retry I'd hope.
							}
						} else {
							errorlog(err2);
						}
					});
			} catch (e) {
				getById("mainmenu").style.opacity = 1;
			}
		})
		.catch(async err => {
			if (retry) {
				console.warn("Couldn't find exact language; trying a more generic option instead");
				lang = lang.split("-")[0];
				if (lang && lang !== "auto") {
					await changeLg(lang, rtl); // won't retry I'd hope.
				}
			} else {
				errorlog(err);
			}
		});
}


var controlBarTimeout = false;
function showControl(e) {
	if (controlBarTimeout) {
		clearTimeout(controlBarTimeout);
	}
	if (session.mobile) {
		getById("controlButtons").classList.remove("partialFadeout");
	} else {
		getById("controlButtons").classList.remove("fadeout");
	}
	controlBarTimeout = setTimeout(function () {
		if (session.mobile) {
			getById("controlButtons").classList.add("partialFadeout");
		} else {
			getById("controlButtons").classList.add("fadeout");
		}
	}, 5000);
}


var loadedQRCode = false;
function loadQR(callback = false, value = false) {
	if (loadedQRCode === false) {
		loadedQRCode = true;
		var script = document.createElement("script");
		if (callback) {
			script.onload = function () {
				callback(value);
			};
		}
		script.src = "./thirdparty/qrcode.min.js"; // dynamically load this only if its needed. Keeps loading time down.
		document.head.appendChild(script);
	} else if (callback) {
		callback(value);
	}
}

var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
	// this listens for child IFRAMES.
	try {
		if (e.origin == "https://www.youtube.com") {
			processYoutubeEvent(e);
		} else if (e.data && typeof e.data == "object" && "action" in e.data) {
			if (e.data.action == "screen-share-state" && !e.data.value) {
				//pokeIframeAPI("screen-share-state", false);
				if (session.screenShareElement && session.screenShareElement.contentWindow) {
					if (e.source == session.screenShareElement.contentWindow) {
						// reject messages send from other iframes
						warnlog(e);
						postMessageIframe(session.screenShareElement, { close: true });
						session.screenShareElement.parentNode.removeChild(session.screenShareElement);
						session.screenShareElement = false;

						updateMixer();
					}
				}
			} else if (e.data.action == "video-loaded") {
				// TODO: if (e.source == session...iframeEle.contentWindow) {
				warnlog(e);
				toggleSpeakerMute(true);
				updateMixer(); // harmless to let run. (not so harmless if updateMixer reloads meshcast actually) TODO; Do I need this?
			}
		}
	} catch (e) {
		errorlog(e);
	}
});

function requestRotateGuest(ele) {
	var UUID = ele.dataset.UUID;
	var data = {};
	//data.mirrorGuestTarget = UUID;
	//session.sendPeers(data, false, UUID);

	ele.classList.add("pressed");
	ele.ariaPressed = "true";

	data.rotate = true;
	session.sendRequest(data, UUID);

	setTimeout(
		function (el) {
			el.value = 0;
			el.classList.remove("pressed");
			el.ariaPressed = "false";
		},
		500,
		ele
	);
}

function requestMirrorGuest(ele) {
	var UUID = ele.dataset.UUID;
	if (ele.value == 1) {
		ele.value = 0;
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		applyMirrorGuest(false, session.rpcs[UUID].videoElement);
		var data = {};
		data.mirrorGuestState = false;

		data.mirrorGuestTarget = UUID;
		session.sendPeers(data, false, UUID);

		data.mirrorGuestTarget = true;
		session.sendPeers(data, UUID);
	} else {
		ele.value = 1;
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
		applyMirrorGuest(true, session.rpcs[UUID].videoElement);
		var data = {};
		data.mirrorGuestState = true;

		data.mirrorGuestTarget = UUID;
		session.sendPeers(data, false, UUID);

		data.mirrorGuestTarget = true;
		session.sendPeers(data, UUID);
	}
}

function requestKeyframeScene(ele) {
	var UUID = ele.dataset.UUID;
	if (ele.value == 1) {
	} else {
		ele.value = 1;
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
		session.requestKeyframe(UUID, true);
		setTimeout(
			function (el) {
				el.value = 0;
				el.classList.remove("pressed");
				el.ariaPressed = "false";
			},
			1000,
			ele
		);
	}
}

function pokeIframeAPI(action, value = null, UUID = null, SID = null, CID = null) {
	if (!isIFrame) {
		return;
	}
	try {
		var data = {};

		data.action = action;

		if (value !== null) {
			data.value = value;
		}
		if (UUID !== null) {
			data.UUID = UUID;
		}
		if (!SID) {
			if (UUID && UUID in session.rpcs) {
				if (session.rpcs[UUID].streamID) {
					SID = session.rpcs[UUID].streamID;
				}
			}
		}
		if (!SID) {
			if (UUID && UUID in session.pcs) {
				if (session.pcs[UUID].streamID) {
					SID = session.pcs[UUID].streamID;
				}
			}
		}

		if (SID) {
			data.streamID = SID;
		}

		if (CID) {
			data.cib = CID;
		}

		if (isIFrame) {
			parent.postMessage(data, session.iframetarget);
		}
	} catch (e) {
		errorlog(e);
	}
}

async function jumptoroom2() {
	var arr = window.location.href.split("?");

	var roomname = getById("videoname1").value;
	roomname = sanitizeRoomName(roomname);
	if (roomname.length) {
		var pass = getById("passwordRoom").value;
		pass = sanitizePassword(pass);

		var passStr = "";
		if (pass && pass.length) {
			passStr = "&password=" + pass;
		}

		if (arr.length > 1 && arr[1] !== "") {
			window.location += "&room=" + roomname + passStr + "&host";
		} else {
			window.location += "?room=" + roomname + passStr + "&host";
		}
	} else {
		getById("videoname1").focus();
		getById("videoname1").classList.remove("shake");
		setTimeout(function () {
			getById("videoname1").classList.add("shake");
		}, 10);
	}
}

async function jumptoroom(event = null) {
	if (event) {
		if (event.which !== 13) {
			return;
		}
	}

	var arr = window.location.href.split("?");
	var roomname = getById("joinroomID").value;
	roomname = sanitizeRoomName(roomname);
	if (roomname.length) {
		var passStr = "";
		window.focus();
		var pass = await promptAlt(getTranslation("enter-password-if-desired"), false, true); //sanitizePassword(session.password);
		if (pass && pass.length) {
			session.password = sanitizePassword(pass);
			passStr = "&password=" + session.password;
		} else {
			session.password = false;
		}

		if (arr.length > 1 && arr[1] !== "") {
			window.location += "&room=" + roomname + passStr;
		} else {
			window.location += "?room=" + roomname + passStr;
		}
	} else {
		getById("joinroomID").focus();
		getById("joinroomID").classList.remove("shake");
		setTimeout(function () {
			getById("joinroomID").classList.add("shake");
		}, 10);
	}
}

async function jumptoURL(event = null) {
	// this is for the native app

	var url = getById("joinbyURL").value;
	if (url.length) {
		if (url.startsWith("?")) {
			url = "./" + url;
		}

		if (url.startsWith("&")) {
			url = "./?" + url;
		}

		if (!url.startsWith("http") && !url.startsWith(".")) {
			url = "./?" + url;
		}

		setStorage("jumptoURL", url, 1008); // should be really only used by the native app; 6 months

		window.location = url;
	} else {
		getById("joinbyURL").focus();
		getById("joinbyURL").classList.remove("shake");
		setTimeout(function () {
			getById("joinbyURL").classList.add("shake");
		}, 10);
	}
}

function sleep(ms = 0) {
	return new Promise(r => setTimeout(r, ms)); // LOLz!
}

function sleepCancellable(ms = 0) {
	let resolve;
	const promise = new Promise(r => {
		resolve = r;
		setTimeout(resolve, ms);
	});
	return { promise, resolve };
}

async function changeAvatarImage(ev, ele, set = false) {
	log("changeAvatarImage() triggered");

	if (session.avatar && session.avatar.timer) {
		clearInterval(session.avatar.timer);
		session.avatar.timer = null;
	}

	if (!session.streamSrc) {
		checkBasicStreamsExist();
	}

	if (ele.files && ele.files.length) {
		session.avatar = document.querySelector("img");
		session.avatar.ready = false;
		session.avatar.onload = () => {
			URL.revokeObjectURL(session.avatar.src); // no longer needed, free memory
			session.avatar.ready = true;
			getById("noAvatarSelected3").classList.remove("selected");
			getById("noAvatarSelected").classList.remove("selected");
			getById("defaultAvatar1").classList.add("selected");
			getById("defaultAvatar2").classList.add("selected");

			var tracks = session.streamSrc.getVideoTracks();
			if (!tracks.length || session.videoMuted) {
				updateRenderOutpipe();
			}
		};

		session.avatar.src = URL.createObjectURL(ele.files[0]); // set src to blob url
		return;
	} else if (ele.tagName.toLowerCase() == "img") {
		session.avatar = ele;
		session.avatar.ready = true;
		getById("noAvatarSelected3").classList.remove("selected");
		getById("noAvatarSelected").classList.remove("selected");
		getById("defaultAvatar1").classList.add("selected");
		getById("defaultAvatar2").classList.add("selected");
		var tracks = session.streamSrc.getVideoTracks();
		if (!tracks.length || session.videoMuted) {
			updateRenderOutpipe();
		}
	} else {
		session.avatar = false;
		var tracks = session.streamSrc.getVideoTracks();
		if (!tracks.length || session.videoMuted) {
			var msg = {};
			msg.videoMuted = true;
			session.sendMessage(msg);
			if (document.getElementById("videosource")) {
				document.getElementById("videosource").load();
			} else if (document.getElementById("previewWebcam")) {
				document.getElementById("previewWebcam").load();
			}
			updateRenderOutpipe();
		}

		getById("noAvatarSelected3").classList.add("selected");
		getById("noAvatarSelected").classList.add("selected");
		getById("defaultAvatar1").classList.remove("selected");
		getById("defaultAvatar2").classList.remove("selected");
		return;
	}
}

session.autoSyncCallback = function (UUID = null) {
	// session.autoSyncObject has been updated.  You can overwrite this with your own function
	log(session.autoSyncObject);
	pokeIframeAPI("auto-sync-updated", session.autoSyncObject, UUID);
};

session.autoSync = function (alternative = null) {
	// Update session.autoSyncObject and then run session.autoSync()
	var msg = {};
	if (alternative === null) {
		msg.autoSync = session.autoSyncObject;
	} else {
		session.autoSyncObject = alternative;
		msg.autoSync = session.autoSyncObject;
	}
	session.sendPeers(msg);
};

//function updateRemotePTZControls(videoOptions, UUID){
//	console.log(videoOptions);
//	console.log(UUID);
//}

function isolateIncomingChannel(channel, UUID) {
    if (!session.rpcs[UUID]) return;

    if (channel === 0 || channel === false) {
        delete session.rpcs[UUID].isolatedChannel;
    } else {
        session.rpcs[UUID].isolatedChannel = channel || session.rpcs[UUID].isolatedChannel;
    }
    updateIncomingAudioElement(UUID);
}

function isolateChannel(source, channel) {
    if (!channel || channel === 0) {
        return source; // No isolation, return the original source
    }
	
    const splitter = session.audioCtx.createChannelSplitter(6); // Assuming max 6 channels
    const merger = session.audioCtx.createChannelMerger(1); // Mono output

    source.connect(splitter);
    splitter.connect(merger, channel - 1, 0); // Connect the specified channel to the mono output

    return merger;
}

function directIsolateChannel(UUID, channel=null){ // isolateChannel()
	try {
		if (UUID){
			
			var targets = document.querySelectorAll("[data--u-u-i-d='" + UUID + "'][data-action-type='isolate-channel']");
			var add = false;
			
			if (channel){
				add = true;
			}
			targets.forEach(ele=>{
				if (channel && parseInt(ele.dataset.channel) && (parseInt(ele.dataset.channel) == channel)){
					
					if (ele.classList.contains("pressed")){
						add = false;
						ele.classList.remove("pressed");
						ele.ariaPressed = "false";
						
					} else {
						ele.classList.add("pressed");
						ele.ariaPressed = "true";
					}
				} else {
					ele.classList.remove("pressed");
					ele.ariaPressed = "false";
				}
			});
			
			
			var msg = {};
			
			if (add){
				msg.isolateChannel = channel
			} else {
				msg.isolateChannel = false;
			}
			session.sendMessage(msg, UUID);
			
			
			
		}
	} catch (e) {
		errorlog(e);
	}
}

function uploadImageSnapshot(PostURL) {
	if (!session.videoElement) {
		return;
	}
	const video = session.videoElement;
	const canvas = document.createElement("canvas");
	canvas.width = video.videoWidth;
	canvas.height = video.videoHeight;
	canvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoHeight); // for drawing the video element on the canvas
	const playImage = new Image();
	canvas.getContext("2d").drawImage(playImage, 0, 0, playImage.width, playImage.height);
	canvas.toBlob(
		function (blob) {
			var request = new XMLHttpRequest();

			if (PostURL.includes("?")) {
				request.open("POST", PostURL + "&name=" + session.streamID);
			} else {
				request.open("POST", PostURL + "?name=" + session.streamID);
			}
			request.setRequestHeader("Content-Type", "image/jpeg");
			request.send(blob);
			log("Posted image");
		},
		"image/jpeg",
		0.9
	);
}

var slideshowImage = false;
var slideshowCanvas = false;
var slideshowActive = false;

function slideshowHack() {
	// just shows an animated IMAGE of the first video. audio plays, but no "video"
	if (!session.manual) {
		session.manual = session.manual === null ? true : session.manual;
	}
	if (slideshowActive) {
		return;
	}
	slideshowActive = true;
	try {
		if (!slideshowImage) {
			slideshowImage = document.createElement("img");
			slideshowImage.style.width = "100%";
			slideshowImage.style.height = "100%";
			slideshowImage.style.objectFit = "contain";
			slideshowImage.style.border = "0";
			slideshowImage.style.padding = "0";
			slideshowImage.style.margin = "0";
			getById("gridlayout").innerHTML = "";
			getById("gridlayout").appendChild(slideshowImage);
		}
		var keys = Object.keys(session.rpcs);
		if (!keys.length) {
			slideshowActive = false;
			return;
		}
		var video = false;
		var key = false;

		for (var i = 0; i < keys.length; i++) {
			if (session.rpcs[keys[i]].videoElement) {
				key = keys[i];
				video = session.rpcs[keys[i]].videoElement;
				break;
			}
		}
		if (!video) {
			slideshowCanvas = false;
			slideshowActive = false;
			return;
		}

		if (!slideshowCanvas) {
			slideshowCanvas = document.createElement("canvas");
			slideshowCanvas.getContext("2d").imageSmoothingEnabled = false;
			session.requestResolution(key, session.viewwidth || 480, session.viewheight || 480);
		}
		slideshowCanvas.width = video.videoWidth;
		slideshowCanvas.height = video.videoHeight;
		slideshowCanvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoHeight); // for drawing the video element on the canvas
		var image = slideshowCanvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); // here is the most important part because if you dont replace you will get a DOM 18 exception.
		slideshowImage.src = image;
	} catch (e) {
		errorlog(e);
	}
	slideshowActive = false;
}

function setAvatarImage(tracks) {
	if (session.avatar && session.avatar.ready) {
		if (session.avatar && session.avatar.timer) {
			clearInterval(session.avatar.timer);
			session.avatar.timer = null;
		}
		setupCanvas();

		var width = 512;
		var height = 288;

		var maxW = 1280;
		var maxH = 720;
		if (session.quality == 0) {
			maxW = 1920;
			maxH = 1080;
		} else if (session.quality == 2) {
			maxW = 640;
			maxH = 360;
		}

		if (session.width) {
			maxW = session.width;
		}
		if (session.height) {
			maxH = session.height;
		}

		if (session.avatar.naturalHeight && session.avatar.naturalHeight > maxH) {
			width = parseInt((maxH / session.avatar.naturalHeight) * session.avatar.naturalWidth);
			height = maxH;

			if (width > maxW) {
				width = maxW;
				height = parseInt((maxW / width) * height);
			}
		} else if (session.avatar.naturalWidth && session.avatar.naturalWidth > maxW) {
			width = maxW;
			height = parseInt((maxW / session.avatar.naturalWidth) * session.avatar.naturalHeight);
		} else {
			width = session.avatar.naturalWidth;
			height = session.avatar.naturalHeight;
		}

		session.canvasSource.width = width;
		session.canvasSource.height = height;

		session.canvas.height = 2 * parseInt(height / 2);
		session.canvas.width = 2 * parseInt(width / 2);

		session.canvasCtx.drawImage(session.avatar, 0, 0, session.canvas.width, session.canvas.height);

		session.avatar.timer = setInterval(function () {
			log("drawing");
			session.canvasCtx.drawImage(session.avatar, 0, 0, session.canvas.width, session.canvas.height);
		}, 200); // too slow and it takes way too long for the video to udpate when a new guest joins

		applyMirror(true);

		session.avatar.tracks = session.canvas.captureStream().getVideoTracks();
		
		return session.avatar.tracks;
	}
	applyMirror(session.mirrorExclude);
	return tracks;
}

var drawOnScreenObject = null;
function drawOnScreen() {
    var canvas = document.getElementById("drawOnScreen");
    if (!canvas) {
        canvas = document.createElement("canvas");
        document.getElementById("gridlayout").appendChild(canvas);
        document.getElementById("gridlayout").style.position = "relative";
    } else {
        return;
    }

    var ctx = canvas.getContext("2d");
    canvas.width = parseInt(document.getElementById("gridlayout").clientWidth / 2);
    canvas.height = parseInt(document.getElementById("gridlayout").clientHeight / 2);
    canvas.style.width = "100%";
    canvas.style.height = "100%";
    canvas.style.display = "block";
    canvas.style.position = "absolute";
    canvas.style.bottom = "0";
    canvas.style.left = "0";

    var flag = false,
        prevX = 0,
        currX = 0,
        prevY = 0,
        currY = 0,
        dot_flag = false;

    var x = "black",
        y = 2;

    var object = {};

    function findxy(res, e) {
        if (res == "down") {
            prevX = currX;
            prevY = currY;
            currX = e.clientX - canvas.offsetLeft;
            currY = e.clientY - canvas.offsetTop;

            flag = true;
            dot_flag = true;
            if (dot_flag) {
                ctx.beginPath();
                ctx.fillStyle = x;
                ctx.fillRect(currX, currY, 2, 2);
                ctx.closePath();
                dot_flag = false;
            }
        }
        if (res == "up" || res == "out") {
            flag = false;
        }
        if (res == "move") {
            if (flag) {
                prevX = currX;
                prevY = currY;
                currX = e.clientX - canvas.offsetLeft;
                currY = e.clientY - canvas.offsetTop;
                draw();
            }
        }
    }

    function draw() {
        ctx.beginPath();

        var mx = canvas.width / parseInt(document.getElementById("gridlayout").clientWidth);
        var my = canvas.height / parseInt(document.getElementById("gridlayout").clientHeight);
        var mo = parseInt(document.getElementById("header").clientHeight);

        ctx.moveTo(prevX * mx, prevY * my - mo * my);
        ctx.lineTo(currX * mx, currY * my - mo * my);
        ctx.strokeStyle = x;
        ctx.lineWidth = y;
        ctx.stroke();
        ctx.closePath();
    }

    function onMouseMove(e) {
        findxy("move", e);
    }

    function onMouseDown(e) {
        findxy("down", e);
    }

    function onMouseUp(e) {
        findxy("up", e);
    }

    function onMouseOut(e) {
        findxy("out", e);
    }

    object.stop = function stop() {
        canvas.removeEventListener("mousemove", onMouseMove, false);
        canvas.removeEventListener("mousedown", onMouseDown, false);
        canvas.removeEventListener("mouseup", onMouseUp, false);
        canvas.removeEventListener("mouseout", onMouseOut, false);
        canvas.remove();

        document.getElementById("startDrawScreen").classList.remove("hidden");

        document.querySelectorAll(".drawActive").forEach(ele => {
            ele.classList.add("hidden");
        });

        drawOnScreenObject = null;
    };

    object.init = function init() {
        canvas.addEventListener("mousemove", onMouseMove, false);
        canvas.addEventListener("mousedown", onMouseDown, false);
        canvas.addEventListener("mouseup", onMouseUp, false);
        canvas.addEventListener("mouseout", onMouseOut, false);

        document.getElementById("startDrawScreen").classList.add("hidden");

        document.querySelectorAll(".drawActive").forEach(ele => {
            ele.classList.remove("hidden");
        });
    };

    object.color = function color(obj) {
        switch (obj.dataset.color) {
            case "green":
                x = "green";
                break;
            case "blue":
                x = "blue";
                break;
            case "red":
                x = "red";
                break;
            case "yellow":
                x = "yellow";
                break;
            case "orange":
                x = "orange";
                break;
            case "black":
                x = "black";
                break;
            case "white":
                x = "white";
                break;
        }
        if (x == "white") y = 14;
        else y = 2;
    };

    object.erase = function erase() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    };

    object.save = function save() {
        var dataURL = canvas.toDataURL();
    };

    object.init();
    drawOnScreenObject = object;
    return object;
}

// SENDER DRAWERS LOGIC PORTION START
function fitCurve(points) {
    if (points.length <= 1) return points;
    if (points.length === 2) return [{t: 'l', p: [points[0], points[1]]}];
    if (points.length === 3) return [{t: 'q', p: points}];

    let result = [];
    for (let i = 0; i < points.length - 1; i += 3) {
        let p0 = points[i];
        let p1 = points[i + 1] || p0;
        let p2 = points[i + 2] || p1;
        let p3 = points[i + 3] || p2;

        result.push({
            t: 'b',
            p: [p0, p1, p2, p3]
        });
    }

    return result;
}

function isSharpTurn(points) {
    if (points.length < 3) return false;
    let angle1 = Math.atan2(points[1].y - points[0].y, points[1].x - points[0].x);
    let angle2 = Math.atan2(points[2].y - points[1].y, points[2].x - points[1].x);
    let angleDiff = Math.abs(angle2 - angle1);
    return angleDiff > Math.PI / 4;
}

function drawOnThis(video) {
    try {
        if (!video || !video.container) {
            warnlog("no video holder; not compatible");
            return;
        }
        var container = video.container || video.parentNode;
        var holder = container.holder || null;

        var canvas = document.createElement('canvas');
        if (!holder) {
            holder = document.createElement("div");
            container.holder = holder;
            holder.className = "holder";
            holder.dataset.holder = true;
            
            container.style = "display: flex;\
                align-items: center;\
                justify-content: center;";
                
            container.appendChild(holder);
            holder.appendChild(video);
            video.style.setProperty('top', '0', 'important');
            video.style.setProperty('left', '0', 'important');
            
            session.windowed = false;
            applyMirror();
            
            canvas.style.position = "fixed";
            holder.style = "position: relative;\
              width: 800px;\
              height: 450px; \
              display: flex;\
              align-items: center;\
              justify-content: center;"
        } else {
            canvas.className = "drawingCanvas";
        }

        canvas.style.pointerEvents = "none";
        holder.appendChild(canvas);
        video.canvas = canvas;

        const ctx = canvas.getContext('2d');
        ctx.lineWidth = 3;

        const buttonContainer = document.createElement('div');
        const enableDrawingBtn = document.createElement('button');
        const clearDrawingBtn = document.createElement('button');
        const undoDrawingBtn = document.createElement('button'); // Undo button

        enableDrawingBtn.textContent = "Enable Drawing";
        clearDrawingBtn.textContent = "Clear";
        undoDrawingBtn.textContent = "Undo"; // Undo button text
        buttonContainer.className = "buttonContainer";

        buttonContainer.appendChild(enableDrawingBtn);
        buttonContainer.appendChild(clearDrawingBtn);
        buttonContainer.appendChild(undoDrawingBtn); // Add undo button to container
        holder.appendChild(buttonContainer);

        let isDrawing = false;
        let drawingEnabled = false;
        let drawingData = [];
        let lastPoint = null;
        let lastSentTime = 0;
        const sendInterval = 1000; // 1 second
        let lastPoints = [];

        function startDrawing(e) {
            if (!drawingEnabled) return;
            isDrawing = true;
            draw(e);
        }

        function draw(e) {
            if (!isDrawing || !drawingEnabled) return;
            
            const rect = canvas.getBoundingClientRect();
            let x = (e.clientX - rect.left) / rect.width;
            let y = (e.clientY - rect.top) / rect.height;
            
            // Check if the mouse is within bounds
            if (x < 0 || x > 1 || y < 0 || y > 1) {
                stopDrawing();
                return;
            }

            
            ctx.lineCap = 'round';
            ctx.strokeStyle = 'red';

            const canvasX = x * canvas.width;
            const canvasY = y * canvas.height;

            if (lastPoint) {
                ctx.beginPath();
                ctx.moveTo(lastPoint.x * canvas.width, lastPoint.y * canvas.height);
                ctx.lineTo(canvasX, canvasY);
                ctx.stroke();
            }

            x = Math.round(x*4000)/4000;
            y = Math.round(y*4000)/4000;
            
            lastPoint = { x, y };
            lastPoints.push({ x, y });

            // Send data more frequently
            if (lastPoints.length >= 5 || Date.now() - lastSentTime >= 50) {
                sendDrawingData();
            }
        }

        function redrawCanvas() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.beginPath();
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';
            ctx.strokeStyle = "red";

            let isNewPath = true;

            for (let i = 0; i < drawingData.length; i++) {
                let segment = drawingData[i];
                
                if (!segment) {
                    // End of a path
                    ctx.stroke();
                    ctx.beginPath();
                    isNewPath = true;
                    continue;
                }

                if (segment.t) {
                    // This is a complex segment (line or bezier)
                    switch(segment.t) {
                        case 'b':
                            let [p0, p1, p2, p3] = segment.p;
                            if (isNewPath) {
                                ctx.moveTo(p0.x * canvas.width, p0.y * canvas.height);
                                isNewPath = false;
                            }
                            ctx.bezierCurveTo(
                                p1.x * canvas.width, p1.y * canvas.height,
                                p2.x * canvas.width, p2.y * canvas.height,
                                p3.x * canvas.width, p3.y * canvas.height
                            );
                            break;
                        case 'q':
                            let [q0, q1, q2] = segment.p;
                            if (isNewPath) {
                                ctx.moveTo(q0.x * canvas.width, q0.y * canvas.height);
                                isNewPath = false;
                            }
                            ctx.quadraticCurveTo(
                                q1.x * canvas.width, q1.y * canvas.height,
                                q2.x * canvas.width, q2.y * canvas.height
                            );
                            break;
                        case 'l':
                            let [l0, l1] = segment.p;
                            if (isNewPath) {
                                ctx.moveTo(l0.x * canvas.width, l0.y * canvas.height);
                                isNewPath = false;
                            }
                            ctx.lineTo(l1.x * canvas.width, l1.y * canvas.height);
                            break;
                        default:
                            warnlog(segment);
                    }
                } else if (segment.x !== undefined && segment.y !== undefined) {
                    // This is a simple point
                    const canvasX = segment.x * canvas.width;
                    const canvasY = segment.y * canvas.height;
                    if (isNewPath) {
                        ctx.moveTo(canvasX, canvasY);
                        isNewPath = false;
                    } else {
                        ctx.lineTo(canvasX, canvasY);
                    }
                }
            }

            ctx.stroke();
        }

        function processPoints(points) {
            let processedPoints = [];
            let currentSegment = [];
            for (let i = 0; i < points.length; i++) {
                if (points[i] === null) {
                    if (currentSegment.length > 0) {
                        if (currentSegment.length === 2) {
                            processedPoints.push({t: 'l', p: currentSegment});
                        } else {
                            processedPoints.push(...fitCurve(currentSegment));
                        }
                        currentSegment = [];
                    }
                    processedPoints.push(null);
                } else if (i > 0 && isSignificantBreak(points[i-1], points[i])) {
                    if (currentSegment.length > 0) {
                        if (currentSegment.length === 2) {
                            processedPoints.push({t: 'l', p: currentSegment});
                        } else {
                            processedPoints.push(...fitCurve(currentSegment));
                        }
                        currentSegment = [];
                    }
                    currentSegment.push(points[i]);
                } else {
                    currentSegment.push(points[i]);
                    if (currentSegment.length >= 4) {
                        processedPoints.push(...fitCurve(currentSegment));
                        currentSegment = [currentSegment[currentSegment.length - 1]];
                    }
                }
            }
            if (currentSegment.length > 0) {
                if (currentSegment.length === 2) {
                    processedPoints.push({t: 'l', p: currentSegment});
                } else {
                    processedPoints.push(...fitCurve(currentSegment));
                }
            }
            return processedPoints;
        }

        function isSignificantBreak(point1, point2) {
            const distance = Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
            return distance > 0.05; // Increased threshold
        }

        function processCurrentSegment(segment) {
            if (segment.length < 3) return segment;

            if (isSharpTurn(segment)) {
                return segment;
            } else {
                return fitCurve(segment);
            }
        }

        function drawBezierCurve(points) {
            const [start, control1, control2, end] = points;
            ctx.moveTo(start.x * canvas.width, start.y * canvas.height);
            ctx.bezierCurveTo(
                control1.x * canvas.width, control1.y * canvas.height,
                control2.x * canvas.width, control2.y * canvas.height,
                end.x * canvas.width, end.y * canvas.height
            );
        }

        function sendDrawingData(alt = false) {
            if (alt === "clear") {
                drawingData = [];
                if (video.id === "videosource") {
                    for (var UUID in session.pcs) {
                        if (session.pcs[UUID].allowDrawing) {  
                            session.sendMessage({ draw: "clear"}, UUID);
                        }
                    }
                } else if (video.id === "screensharesource") {
                    for (var UUID in session.pcs) {
                        if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { 
                            session.sendMessage({ draw: "clear", altUUID:true}, UUID);
                        }
                    }
                } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
                    session.sendRequest({ draw: "clear" }, video.dataset.UUID);
                }
                return;
            }
			if (alt === "cleanup") {
                drawingData = [];
                if (video.id === "videosource") {
                    for (var UUID in session.pcs) {
                        if (session.pcs[UUID].allowDrawing) {  
                            session.sendMessage({ draw: "cleanup"}, UUID);
                        }
                    }
                } else if (video.id === "screensharesource") {
                    for (var UUID in session.pcs) {
                        if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { 
                            session.sendMessage({ draw: "cleanup", altUUID:true}, UUID);
                        }
                    }
                } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
                    session.sendRequest({ draw: "cleanup" }, video.dataset.UUID);
                }
                return;
            }
            if (alt === "undo") {
                if (drawingData.length > 0) {
                    // Find the last segment to remove
					let found = false;
                    for (let i = drawingData.length - 1; i >= 0; i--) {
                        if (drawingData[i] === null) {
                            drawingData = drawingData.slice(0, i);
							if (found){
								drawingData.push(null);
								break;
							}
                        } else {
							found = true;
						}
                        if (i === 0) { // Handle case when there's no null in drawingData
                            drawingData = [];
                        }
                    }
					redrawCanvas();
					
                    // Send the undo command
                    if (video.id === "videosource") {
                        for (var UUID in session.pcs) {
                            if (session.pcs[UUID].allowDrawing) {
                                session.sendMessage({ draw: "undo" }, UUID);
                            }
                        }
                    } else if (video.id === "screensharesource") {
                        for (var UUID in session.pcs) {
                            if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) {
                                session.sendMessage({ draw: "undo", altUUID: true }, UUID);
                            }
                        }
                    } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
                        session.sendRequest({ draw: "undo" }, video.dataset.UUID);
                    }
                    
                }
                return;
            }
			if (alt === "sync") {
				if (!drawingData.length){return;}
				// Send the processed points
                if (video.id === "videosource") {
                    for (var UUID in session.pcs) {
                        if (session.pcs[UUID].allowDrawing) {
							if (!session.pcs[UUID].initialDrawing){
								session.pcs[UUID].initialDrawing = true;
								session.sendMessage({ draw: {p: drawingData}}, UUID);
							}
                        }
                    }
                } else if (video.id === "screensharesource") {
                    for (var UUID in session.pcs) {
                        if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) {
							if (!session.pcs[UUID].initialDrawing2){
								session.pcs[UUID].initialDrawing2 = true;
								session.sendMessage({ draw: {p: drawingData}, altUUID: true}, UUID);
							}
                        }
                    }
                } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
					if (!session.rpcs[video.dataset.UUID].initialDrawing){
						session.rpcs[video.dataset.UUID].initialDrawing = true;
						session.sendRequest({ draw: {p: drawingData} }, video.dataset.UUID);
					}
                }
				return;
			}
            if (lastPoints.length > 0) {
                var processedPoints = processPoints(lastPoints);
                lastPoints = [];
                lastSentTime = Date.now();
                
                var dataToSend = {
                    p: processedPoints
                };
                
                drawingData.push(...processedPoints); // Store only points in drawingData

                // Send the processed points with timestamp
                if (video.id === "videosource") {
                    for (var UUID in session.pcs) {
                        if (session.pcs[UUID].allowDrawing) {
							if (session.pcs[UUID].initialDrawing){
								session.sendMessage({ draw: dataToSend}, UUID);
							} else {
								session.pcs[UUID].initialDrawing = true;
								session.sendMessage({ draw: {p: drawingData}}, UUID);
							}
                        }
                    }
                } else if (video.id === "screensharesource") {
                    for (var UUID in session.pcs) {
                        if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) {
							if (session.pcs[UUID].initialDrawing2){
								session.sendMessage({ draw: dataToSend, altUUID: true}, UUID);
							} else {
								session.pcs[UUID].initialDrawing2 = true;
								session.sendMessage({ draw: {p: drawingData}, altUUID: true}, UUID);
							}
                        }
                    }
                } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
					if (session.rpcs[video.dataset.UUID].initialDrawing){
						session.sendRequest({ draw: dataToSend }, video.dataset.UUID);
					} else {
						session.rpcs[video.dataset.UUID].initialDrawing = true;
						session.sendRequest({ draw: {p: drawingData} }, video.dataset.UUID);
					}
                }
            }
        }
        
        function resizeCanvas() {
            canvas.width = video.offsetWidth;
            canvas.height = video.offsetHeight;
            redrawCanvas();
        }

        resizeCanvas();
        window.addEventListener('resize', resizeCanvas);
        video.addEventListener('resize', resizeCanvas);

        enableDrawingBtn.addEventListener('click', () => {
            drawingEnabled = !drawingEnabled;
            enableDrawingBtn.textContent = drawingEnabled ? 'Disable Drawing' : 'Enable Drawing';
            canvas.style.pointerEvents = drawingEnabled ? "auto" : "none";
        });

        clearDrawingBtn.addEventListener('click', () => {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            drawingData = [];
            lastPoints = [];
            sendDrawingData("clear");
        });

        undoDrawingBtn.addEventListener('click', () => {
            sendDrawingData("undo");
        });
        
        function stopDrawing() {
            if (isDrawing) {
                
                isDrawing = false;
                ctx.beginPath();
                lastPoint = null;
                lastPoints.push(null); // Add null to mark end of path
                sendDrawingData();
            }
        }
        
        canvas.addEventListener('mousedown', startDrawing);
        canvas.addEventListener('mousemove', draw);
        canvas.addEventListener('mouseup', stopDrawing);
        canvas.addEventListener('mouseout', stopDrawing);
        canvas.addEventListener('mouseleave', stopDrawing);
        canvas.addEventListener('mouseenter', (e) => {
            if (e.buttons !== 1) { // If left mouse button is not pressed
                stopDrawing();
            }
        });

        function createCleanupFunction() {
            return function cleanup() {
				sendDrawingData("cleanup");

				window.removeEventListener('resize', resizeCanvas);
				video.removeEventListener('resize', resizeCanvas);
				
				if (canvas) {
					canvas.removeEventListener('mousedown', startDrawing);
					canvas.removeEventListener('mousemove', draw);
					canvas.removeEventListener('mouseup', stopDrawing);
					canvas.removeEventListener('mouseout', stopDrawing);
					canvas.removeEventListener('mouseleave', stopDrawing);
					canvas.removeEventListener('mouseenter', stopDrawing);
					
					if (canvas.parentNode) {
						canvas.parentNode.removeChild(canvas);
					}
				}
				
				if (buttonContainer && buttonContainer.parentNode) {
					buttonContainer.parentNode.removeChild(buttonContainer);
				}
            };
        }
		
		function syncNewConnections() {
			return function syncDrawing() {
				setTimeout(()=>{
					sendDrawingData("sync")
				},4000);
			}
		}
		
		video.syncDrawOnVideo = syncNewConnections();
        video.clearDrawOnVideo = createCleanupFunction();
        return video.clearDrawOnVideo;
    
    } catch (e) {
        errorlog(e);
    }
}

// END SENDING LOGIC
// var cleanUp = drawOnThis(document.getElementById('videoElement'));
// cleanUp();
// START RECEIVING LOGIC

function receiveDrawingOnVideo(video, UUID = false) {
    try {
        if (!video || !video.container) {
            warnlog("no video holder; not compatible");
            return;
        }
        const canvas = document.createElement('canvas');
        canvas.className = "drawingCanvas";
        canvas.style.pointerEvents = "none";
        var receivedDrawingData = [];

        var container = video.parentNode;
        if (!container) {
            return;
        }
        container.appendChild(canvas);

        var color = 'red';
        if (UUID) {
            color = getColorFromName(UUID);
        }

        function positionCanvas() {
            const videoRect = video.getBoundingClientRect();
            const computedStyle = getComputedStyle(video);

            canvas.style.width = computedStyle.width;
            canvas.style.height = computedStyle.height;
            canvas.style.top = `${videoRect.top + window.scrollY}px`;
            canvas.style.left = `${videoRect.left + window.scrollX}px`;
            canvas.width = video.clientWidth;
            canvas.height = video.clientHeight;

            if (video.dataset.transform) {
                canvas.style.transform = video.dataset.transform;
            }
            redrawCanvas();
        }

        const ctx = canvas.getContext('2d');
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.imageSmoothingEnabled = true;
        ctx.imageSmoothingQuality = 'high';
        ctx.lineWidth = 3;
        

        function resizeCanvas() {
            canvas.width = video.offsetWidth;
            canvas.height = video.offsetHeight;
            if (video.dataset.transform) {
                canvas.style.transform = video.dataset.transform;
            }
            redrawCanvas();
        }

        let observer = null;
        if (!(video && video.container && video.container.holder)) {
            positionCanvas();
            window.addEventListener('resize', positionCanvas);
            video.addEventListener('resize', positionCanvas);
            observer = new ResizeObserver(positionCanvas);
            observer.observe(video);
        } else {
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            video.addEventListener('resize', resizeCanvas);
        }

        function drawBezierCurve(points) {
            const [start, control1, control2, end] = points;
            ctx.moveTo(start.x * canvas.width, start.y * canvas.height);
            ctx.bezierCurveTo(
                control1.x * canvas.width, control1.y * canvas.height,
                control2.x * canvas.width, control2.y * canvas.height,
                end.x * canvas.width, end.y * canvas.height
            );
        }

        function redrawCanvas() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.beginPath();
            ctx.lineWidth = 2;
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';
            ctx.strokeStyle = color;
            
            let isNewPath = true;

            for (let i = 0; i < receivedDrawingData.length; i++) {
                let segment = receivedDrawingData[i];
                
                if (!segment) {
                    // End of a path
                    ctx.stroke();
                    ctx.beginPath();
                    isNewPath = true;
                    continue;
                }

                switch(segment.t) {
                    case 'b':
                        let [p0, p1, p2, p3] = segment.p;
                        if (isNewPath) {
                            ctx.moveTo(p0.x * canvas.width, p0.y * canvas.height);
                            isNewPath = false;
                        }
                        ctx.bezierCurveTo(
                            p1.x * canvas.width, p1.y * canvas.height,
                            p2.x * canvas.width, p2.y * canvas.height,
                            p3.x * canvas.width, p3.y * canvas.height
                        );
                        break;
                    case 'q':
                        let [q0, q1, q2] = segment.p;
                        if (isNewPath) {
                            ctx.moveTo(q0.x * canvas.width, q0.y * canvas.height);
                            isNewPath = false;
                        }
                        ctx.quadraticCurveTo(
                            q1.x * canvas.width, q1.y * canvas.height,
                            q2.x * canvas.width, q2.y * canvas.height
                        );
                        break;
                    case 'l':
                        let [l0, l1] = segment.p;
                        if (isNewPath) {
                            ctx.moveTo(l0.x * canvas.width, l0.y * canvas.height);
                            isNewPath = false;
                        }
                        ctx.lineTo(l1.x * canvas.width, l1.y * canvas.height);
                        break;
                    default:
                        // Fallback for non-curve points
                        if (isNewPath) {
                            ctx.moveTo(segment.x * canvas.width, segment.y * canvas.height);
                            isNewPath = false;
                        } else {
                            ctx.lineTo(segment.x * canvas.width, segment.y * canvas.height);
                        }
                }
            }

            ctx.stroke();
        }

        function updateDrawing(newData) {
            if (newData === "clear") {
                receivedDrawingData = [];
                ctx.clearRect(0, 0, canvas.width, canvas.height);
            } else if (newData === "undo") {
                if (receivedDrawingData.length > 0) {
                    // Find the last segment to remove
					let found = false;
                    for (let i = receivedDrawingData.length - 1; i >= 0; i--) {
                        if (receivedDrawingData[i] === null) {
                            receivedDrawingData = receivedDrawingData.slice(0, i);
							if (found){
								receivedDrawingData.push(null);
								break;
							}
                        } else {
							found = true;
						}
                        if (i === 0) { // Handle case when there's no null in receivedDrawingData
                            receivedDrawingData = [];
                        }
                    }
                    redrawCanvas();
                }
            } else {
                // Handle both new and old data formats 
                if (newData.p) {
                    // New format
                    receivedDrawingData.push(...newData.p);
                } else if (Array.isArray(newData)) {
                    // Old format or array of points
                    receivedDrawingData.push(...newData);
                } else if (typeof newData === 'object' && newData.x !== undefined && newData.y !== undefined) {
                    // Single point
                    receivedDrawingData.push(newData);
                } else {
                    console.error("Unexpected data format:", newData);
                    return;
                }
                
                redrawCanvas();
            }
        }

        function clearDrawing() {
            receivedDrawingData = [];
            ctx.clearRect(0, 0, canvas.width, canvas.height);
        }

        function cleanup() {
			
			try {
				if (observer) {
					window.removeEventListener('resize', positionCanvas);
					video.removeEventListener('resize', positionCanvas);
					observer.disconnect();
				} else {
					window.removeEventListener('resize', resizeCanvas);
					video.removeEventListener('resize', resizeCanvas);
				}
				
				clearDrawing(); 
			} catch(e){
				errorlog(e);
			}
			
            container.removeChild(canvas);
        }

        return {
            updateDrawing,
            clearDrawing,
            cleanup
        };
    } catch (e) {
        errorlog(e);
    }
}

// END REMOTE DRAWING LOGIC


//////////  Canvas Effects  ///////////////

var drawFrameMirroredActive = false;
function drawFrameMirrored(mirror = true, flip = false) {
	if (drawFrameMirroredActive) return;
	drawFrameMirroredActive = true;

	if (session.effect == "2") {
		mirror = true;
		flip = false;
	} else if (session.effect == "-2") {
		mirror = true;
		flip = true;
	} else if (session.effect == "-1") {
		mirror = false;
		flip = true;
	}

	try {
		session.canvasCtx.save();
		if (flip) {
			session.canvasCtx.scale(mirror ? -1 : 1, -1);
			session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * (mirror ? -1 : 1), session.canvas.height * -1);
		} else {
			session.canvasCtx.scale(mirror ? -1 : 1, 1);
			session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * (mirror ? -1 : 1), session.canvas.height);
		}
		session.canvasCtx.restore();
	} catch (e) {
		errorlog(e);
	}

	drawFrameMirroredActive = false;
}

function motionDetection(video, threshold = 15, sensitivity = 75) {
	var targetSize = 16;

	if (!video.motionDetector) {
		video.motionDetector = {};
		video.motionDetector.canvas = document.createElement("canvas");
		video.motionDetector.canvas.width = targetSize;
		video.motionDetector.canvas.height = targetSize;
		try {
			video.motionDetector.ctx = video.motionDetector.canvas.getContext("2d", { willReadFrequently: true });
		} catch (e) {
			video.motionDetector.ctx = video.motionDetector.canvas.getContext("2d");
		}
		video.motionDetector.previous = [];
		for (var y = 0; y < targetSize; y++) {
			for (var x = 0; x < targetSize; x++) {
				video.motionDetector.previous.push(0);
			}
		}
	}
	var motionDetector = video.motionDetector;

	motionDetector.ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, targetSize, targetSize);
	var data = motionDetector.ctx.getImageData(0, 0, targetSize, targetSize).data;
	var matches = 0;
	for (var y = 0; y < targetSize; y++) {
		for (var x = 0; x < targetSize; x++) {
			var pos = y * targetSize + x;
			var pos2 = pos * 3;
			var value = data[pos2] + data[pos2 + 1] + data[pos2 + 2]; // convert to to greyscale
			if (motionDetector.previous[pos] && Math.abs(motionDetector.previous[pos] - value) > sensitivity) {
				matches += 1;
			}
			motionDetector.previous[pos] = value;
		}
	}
	if (matches >= threshold) {
		log("MOTION DETECTED: " + matches);
		if (session.motionSwitch && window.obsstudio && window.obsstudio["setCurrentScene"]) {
			if (!changeSceneEnabled) {
				// the bit cut scene change is already active.
				if (session.obsState && session.obsState.details && session.obsState.details.thisScene && session.obsState.details.currentScene) {
					if (session.obsState.details.thisScene !== session.obsState.details.currentScene.name) {
						// don't trigger it multiple times; makes it hard to prep next scene
						window.obsstudio["setCurrentScene"](session.obsState.details.thisScene);
					}
				}
			}
		}
		pokeIframeAPI("motion-detected", true, video.dataset.UUID || true);

		if (session.infocus !== (video.dataset.UUID || true)) {
			if (!session.layout) {
				session.infocus = video.dataset.UUID || true;
				updateMixer();
			}
		}

		if (session.motionRecord) {
			if (!session.motionRecordTimeout) {
				session.motionRecordTimeout = setTimeout(function () {
					session.motionRecordTimeout = null;
				}, 1000);
				saveVideoFrameToDisk(video);
			}
		}
	}
}

let currentOscillatorId = 0;
function clearOscillator() {
	const thisOscillatorId = ++currentOscillatorId;
	if (session.canvasOscillator) {
		session.canvasOscillator.stop();
		session.canvasOscillator.disconnect();
		session.canvasOscillator = null;
	}

	if (session.canvasSilence) {
		session.canvasSilence.disconnect();
		session.canvasSilence = null;
	}
	if (session.stats) {
		delete session.stats.canvas_draw_rate;
	}
}
function setupOscillator(callbackFunction, frameRate = 30, timeOne = null, thisOscillatorId = null) {
	if (!thisOscillatorId) {
		thisOscillatorId = ++currentOscillatorId;
	} else if (currentOscillatorId !== thisOscillatorId) {
		return false;
	}

	if (session.canvasOscillator) {
		session.canvasOscillator.stop();
		session.canvasOscillator.disconnect();
		session.canvasOscillator = null;
	}

	if (session.canvasSilence) {
		session.canvasSilence.disconnect();
		session.canvasSilence = null;
	}

	if (!session.audioCtx) {
		session.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
	}

	let oscillator = session.audioCtx.createOscillator();
	session.canvasOscillator = oscillator;
	let silence = session.audioCtx.createGain();
	session.canvasSilence = silence;

	silence.gain.value = 0;
	oscillator.connect(silence);
	silence.connect(session.audioCtx.destination);

	if (!timeOne) {
		timeOne = session.audioCtx.currentTime;
	}

	oscillator.onended = () => {
		oscillator.disconnect();
		silence.disconnect();
		if (currentOscillatorId === thisOscillatorId) {
			let timeTwo = session.audioCtx.currentTime;

			if (typeof callbackFunction === "function") {
				callbackFunction();
			}

			if (session.stats) {
				let actualRate = 1 / (timeTwo - timeOne); // Calculate the actual FPS
				session.stats.canvas_draw_rate = parseInt(actualRate * 10) / 10;
			}
			setupOscillator(callbackFunction, frameRate, timeTwo, thisOscillatorId);
		}
	};

	oscillator.start(timeOne);
	oscillator.stop(timeOne + 1 / frameRate);

	return function (check = false) {
		if (check && currentOscillatorId !== thisOscillatorId) {
			clearOscillator();
			return true;
		} else if (check) {
			return false;
		}
		if (currentOscillatorId === thisOscillatorId) {
			clearOscillator(); // clear only if needs to be cleared
		}
		return false;
	};
}

function setupCanvas() {
	clearOscillator();

	log("SETUP CANVAS");

	if (session.canvas === null) {
		session.canvas = document.createElement("canvas");
		session.canvas.width = 512;
		session.canvas.height = 288;
		try {
			session.canvasCtx = session.canvas.getContext("2d", { alpha: true, willReadFrequently: true });
		} catch (e) {
			errorlog(e);
			session.canvasCtx = session.canvas.getContext("2d");
		}

		session.canvasCtx.fillStyle = "black";
		session.canvasCtx.fillRect(0, 0, 512, 288);
		session.canvasSource = createVideoElement();
		session.canvasSource.autoplay = true;
		session.canvasSource.srcObject = createMediaStream();
		session.canvasSource.id = "effectsVideoSource";

		if (session.canvasSource.srcObject.getVideoTracks().length) {
			session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
			session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
		}

		if (iOS || iPad) {
			session.canvasSource.style.position = "absolute";
			session.canvasSource.style.left = "0";
			session.canvasSource.style.top = "0";
			session.canvasSource.controls = session.showControls || false;
			session.canvasSource.style.maxWidth = "1px";
			session.canvasSource.style.maxHeight = "1px";
			session.canvasSource.setAttribute("playsinline", "");
			document.body.appendChild(session.canvasSource);
			//session.canvasSource.play();
		}
	} else {
		session.canvasSource.srcObject.getVideoTracks().forEach(function (trk) {
			session.canvasSource.srcObject.removeTrack(trk);
		});
	}
}

function applyEffects(track) {
	// video only please. do not touch audio.  Run update Render Outpipe () instead of this directly.
	log("applyEffects()");

	if (session.effect == "0" || !session.effect) {
		// auto align face
		return track;
	} else if (session.effect == "1") {
		// auto align face
		setupCanvas();
		session.canvasSource.srcObject.addTrack(track);

		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;

		session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
		session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

		drawFace();
	} else if (session.effect == "7") {
		// manual zoom
		setupCanvas();
		session.canvasSource.srcObject.addTrack(track);

		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;
		session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
		session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

		digitalZoom(true);
	} else if (["8", "overlay"].includes(session.effect)) {
		// manual zoom
		setupCanvas();
		session.canvasSource.srcObject.addTrack(track);

		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;
		session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
		session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

		simpleDraw();
	} else if (["-2", "-1", "2"].includes(session.effect)) {
		// mirror video at a canvas level
		setupCanvas();
		session.canvasSource.srcObject.addTrack(track);

		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;
		session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
		session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

		setupOscillator(drawFrameMirrored, track.getSettings().frameRate || 30);
	} else if (session.effect == "3" || session.effect == "4" || session.effect == "5") {
		// blur & greenscreen (low and high)
		setupCanvas();
		session.canvasSource.srcObject.addTrack(track);

		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;
		session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
		session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

		TFLiteWorker();
	} else if (session.effect == "6") {
		setupCanvas();
		session.canvasSource.srcObject.addTrack(track);

		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;
		session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
		session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

		if (session.canvasSource.readyState >= 3) {
			mainMeshMask();
		} else {
			session.canvasSource.onloadeddata = mainMeshMask;
		}
	} else if (session.effect == "13") {
		// ha, no way to turn it off once it's started, except to change cameras? not sure.
		try {
			track
				.applyConstraints({ backgroundBlur: true })
				.then(() => {
					const settings = track.getSettings();
					log(`Background blur is ${settings.backgroundBlur ? "ON" : "OFF"}`);
				})
				.catch(errorlog);
		} catch (e) {
			errorlog(e);
		}
		return track;
	} else if (session.effect == "14" || session.effect == "15") {
		// chroma key effects
		setupCanvas();
		session.canvasSource.srcObject.addTrack(track);
		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;
		session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
		session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
		chromaKey();
	} else {
		if (session.canvasource) {
			session.canvasSource.srcObject.getVideoTracks().forEach(function (trk) {
				session.canvasSource.srcObject.removeTrack(trk);
			});
		} else {
			session.canvasSource = createVideoElement();
			session.canvasSource.srcObject = createMediaStream();
		}

		session.canvasSource.autoplay = true;
		session.canvasSource.id = "effectsVideoSource";
		session.canvasSource.srcObject.addTrack(track);
		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;
		session.canvas.width = 512;
		session.canvas.height = 288;

		if (iOS || iPad) {
			session.canvasSource.style.position = "absolute";
			session.canvasSource.style.left = "0";
			session.canvasSource.style.top = "0";
			session.canvasSource.style.maxWidth = "1px";
			session.canvasSource.style.maxHeight = "1px";
			session.canvasSource.controls = session.showControls || false;

			session.canvasSource.setAttribute("playsinline", "");
			document.body.appendChild(session.canvasSource);
			//session.canvasSource.play();
		}

		try {
			JEELIZFACEFILTER.destroy();
		} catch (e) {}
		if (session.canvasWebGL) {
			session.canvasWebGL.remove();
			session.canvasWebGL = null;
		}
		session.canvasWebGL = document.createElement("canvas");
		session.canvasWebGL.width = track.getSettings().width || 1280;
		session.canvasWebGL.height = track.getSettings().height || 720;
		session.canvasWebGL.id = "effectsCanvasTarget";
		session.canvasWebGL.style.position = "fixed";
		session.canvasWebGL.style.top = "-9999px";
		session.canvasWebGL.style.left = "-9999px";

		document.body.appendChild(session.canvasWebGL);
		loadEffect(session.effect);
		
		return session.canvasWebGL.captureStream().getVideoTracks()[0];
	}
	try {
		return session.canvas.captureStream().getVideoTracks()[0];
		
	} catch (e) {
		if (!session.cleanOutput) {
			warnUser(getTranslation("not-clean-session"), false, false);
		}
		return track;
	}
}

function dataURItoArraybuffer(dataURI) {
	var byteString = atob(dataURI.split(",")[1]);
	var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
	var ab = new ArrayBuffer(byteString.length);
	var ia = new Uint8Array(ab);
	for (var i = 0; i < byteString.length; i++) {
		ia[i] = byteString.charCodeAt(i);
	}
	return ab;
}

var makeImagesActive = null;
async function makeImages(startup = false) {
	if (!session.webp) {
		return;
	} else if (makeImagesActive === true) {
		return;
	} else if (!session.videoElement) {
		return;
	} else if (session.videoMuted) {
		return;
	}

	var stream = session.getLocalStream();

	if (!stream || !stream.getVideoTracks().length) {
		errorlog("No video element; can't make images for webp mode");
		if (makeImagesActive) {
			var exit = true;
			for (var i in session.pcs) {
				if (session.pcs[i].allowWebp) {
					// just for safety, to avoid a race condition, double check that it's still not active.
					exit = false;
				}
			}
			if (exit) {
				makeImagesActive = false;
				return;
			}

			if (session.webPcanvas.makeImagesTimeout) {
				session.webPcanvas.makeImagesTimeout.onended = null;
				session.webPcanvas.makeImagesTimeout = null;
			}

			var osc = session.webPcanvas.aCtx.createOscillator();
			osc.connect(session.webPcanvas.silence);
			session.webPcanvas.makeImagesTimeout = osc;
			osc.start(0);
			osc.onended = function () {
				this.disconnect();
				makeImages();
			};
			osc.stop(session.webPcanvas.aCtx.currentTime + 0.5);
		}
		return;
	}

	if (makeImagesActive === null) {
		makeImagesActive = true;
		session.webPcanvas = document.createElement("canvas");
		session.webPcanvas.makeImagesTimeout = null;

		session.webPcanvas.aCtx = new AudioContext();
		session.webPcanvas.silence = session.webPcanvas.aCtx.createGain();
		session.webPcanvas.silence.gain.value = 0;
		session.webPcanvas.silence.connect(session.webPcanvas.aCtx.destination);

		session.webPcanvas.nowTime = new Date().getTime();

		session.webPcanvasCtx = session.webPcanvas.getContext("2d", { alpha: session.alpha });
	} else {
		if (session.webPcanvas.makeImagesTimeout) {
			session.webPcanvas.makeImagesTimeout.onended = null;
		}
		makeImagesActive = true;
	}

	if (startup) {
		var exit = true;
		for (var i in session.pcs) {
			if (session.pcs[i].allowWebp) {
				// just for safety, to avoid a race condition, double check that it's still not active.
				exit = false;
			}
		}
		if (exit) {
			makeImagesActive = false;
			return;
		}
		log("MAKE IMAGES STARTING?");
	}

	var track = stream.getVideoTracks()[0];
	var settings = track.getSettings();

	try {
		var broadcasting = false;
		var arrayBuffer = false;

		var width = 480;
		var height = 270;
		var timeout = settings.frameRate > 24 ? 1000 / 24 : 1000 / settings.frameRate; // the answer to everything.
		var quality = 0.66;

		if (session.webPquality === 0) {
			width = 1920;
			height = 1080;
			timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
		} else if (session.webPquality === 1) {
			width = 1280;
			height = 720;
			timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
		} else if (session.webPquality === 2) {
			width = 960;
			height = 540;
			timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
		} else if (session.webPquality === 3) {
			width = 853;
			height = 480;
			timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
		} else if (session.webPquality === 4) {
			width = 640;
			height = 360;
			timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
		} else if (session.webPquality === 5) {
			width = 480;
			height = 270;
			timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
		} else if (session.webPquality === 6) {
			width = 480;
			height = 270;
			timeout = 1000 / 15;
		} else if (session.webPquality === 7) {
			width = 480;
			height = 270;
			timeout = 1000 / 5;
		} else if (session.webPquality === 8) {
			width = 480;
			height = 270;
			timeout = 1000 / 3;
		} else if (session.webPquality === 9) {
			width = 640;
			height = 360;
			timeout = 1000;
		}

		session.webPcanvas.timeout = timeout;
		session.webPcanvas.quality = quality;

		if (settings.width < width) {
			session.webPcanvas.width = settings.width;
			session.webPcanvasCtx.width = settings.width;
		} else {
			session.webPcanvas.width = width;
			session.webPcanvasCtx.width = width;
		}

		if (settings.height < height) {
			session.webPcanvas.height = settings.height;
			session.webPcanvasCtx.height = settings.height;
		} else {
			session.webPcanvas.height = height;
			session.webPcanvasCtx.height = height;
		}

		var ar = session.webPcanvas.width / session.webPcanvas.height;

		if (session.forceAspectRatio && session.forceAspectRatio > ar) {
			session.webPcanvas.width = session.webPcanvas.height * session.forceAspectRatio;
		} else if (session.forceAspectRatio && session.forceAspectRatio <= ar) {
			session.webPcanvas.height = session.webPcanvas.width * session.forceAspectRatio;
		}

		for (var i in session.pcs) {
			try {
				if (session.pcs[i].allowWebp) {
					// only publish to those seeking this stream
					broadcasting = true;
					if (!session.pcs[i].sendChannel.bufferedAmount) {
						try {
							if (!arrayBuffer) {
								session.webPcanvasCtx.drawImage(session.videoElement, 0, 0, session.webPcanvas.width, session.webPcanvas.height);
								arrayBuffer = dataURItoArraybuffer(session.webPcanvas.toDataURL("image/" + session.webp, session.webPcanvas.quality));
							}
							session.pcs[i].sendChannel.send(arrayBuffer);
						} catch (e) {
							errorlog(e);
						}
					}
				}
			} catch (e) {}
		}
	} catch (e) {
		errorlog(e);
		makeImagesActive = false;
		return;
	}
	makeImagesActive = false;

	session.webPcanvas.lastTime = session.webPcanvas.nowTime;
	session.webPcanvas.nowTime = new Date().getTime();

	if (broadcasting) {
		// wait a bit of time, now that we sent a frame out.

		var time = session.webPcanvas.timeout - (session.webPcanvas.nowTime - session.webPcanvas.lastTime);
		if (time <= 0) {
			var osc = session.webPcanvas.aCtx.createOscillator();
			osc.connect(session.webPcanvas.silence);
			session.webPcanvas.makeImagesTimeout = osc;
			osc.start(0);
			osc.onended = function () {
				this.disconnect();
				makeImages();
			};
			osc.stop(session.webPcanvas.aCtx.currentTime);

			//session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},0);
		} else {
			var osc = session.webPcanvas.aCtx.createOscillator();
			osc.connect(session.webPcanvas.silence);
			session.webPcanvas.makeImagesTimeout = osc;
			osc.start(0);
			osc.onended = function () {
				this.disconnect();
				makeImages();
			};
			osc.stop(session.webPcanvas.aCtx.currentTime + time / 1000);

			//session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},time);
		}
	} else {
		// just double check that we shoulnd't be broadcasting.
		for (var i in session.pcs) {
			if (session.pcs[i].allowWebp) {
				var osc = session.webPcanvas.aCtx.createOscillator();
				osc.connect(session.webPcanvas.silence);
				session.webPcanvas.makeImagesTimeout = osc;
				osc.start(0);
				osc.onended = function () {
					this.disconnect();
					makeImages();
				};
				osc.stop(session.webPcanvas.aCtx.currentTime + time / 1000);
				//session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},0);
				return;
			}
		}
		log("Stopping webP broadcast.");
	}
}

var updateUserListTimeout = null;
var updateUserListActive = false;
function updateUserList() {
	if (session.showList === true) {
		// continue
	} else if (session.showList !== true && (session.cleanOutput || session.scene !== false || !session.roomid || session.director || session.showList === false)) {
		return;
	}
	clearInterval(updateUserListTimeout);
	updateUserListTimeout = setTimeout(function () {
		if (updateUserListActive) {
			return;
		}
		updateUserListActive = true;
		try {
			var added = false;
			getById("userList").innerHTML = "";

			for (var UUID in session.rpcs) {
				if ((session.rpcs[UUID].videoElement && session.rpcs[UUID].streamSrc && session.rpcs[UUID].streamSrc.getTracks().length) || session.rpcs[UUID].canvas || session.rpcs[UUID].imageElement) {
					if (session.rpcs[UUID].videoElement && document.body.contains(session.rpcs[UUID].videoElement)) {
						continue;
					} else if (session.rpcs[UUID].canvas && document.body.contains(session.rpcs[UUID].canvas)) {
						continue;
					} else if (session.rpcs[UUID].imageElement && document.body.contains(session.rpcs[UUID].imageElement)) {
						continue;
					}
				}
				if (session.rpcs[UUID].virtualHangup) {
					// end of screen share / director ?
					continue;
				}
				if (session.rpcs[UUID].videoMuted || (!session.rpcs[UUID].imageElement && !session.rpcs[UUID].canvas) || (session.infocus && session.infocus !== UUID) || (!session.rpcs[UUID].defaultSpeaker && session.activeSpeaker)) {
					if (session.directorList.indexOf(UUID) >= 0) {
						if (!session.rpcs[UUID].streamSrc) {
							// director not active yet, so we won't bother showing it.
							continue;
						}
					}

					var insert = document.createElement("div");
					if (session.rpcs[UUID].label) {
						insert.innerText = session.rpcs[UUID].label.split("\\n")[0] + "";
					} else if (session.directorList.indexOf(UUID) >= 0) {
						miniTranslate(insert, "director");
						//insert.innerHTML = getTranslation("director");
					} else {
						miniTranslate(insert, "unknown-user");
						//insert.innerHTML = getTranslation("unknown-user");
					}
					try {
						insert.dataset.UUID = UUID;
						insert.dataset.sid = session.rpcs[UUID].streamID;
						insert.title = "Stream ID: " + session.rpcs[UUID].streamID;

						insert.addEventListener("click", function (e) {
							// show stats of video if double clicked
							log("clicked");
							try {
								if (session.statsMenu !== false) {
									var uid = e.currentTarget.dataset.UUID;
									if (e.ctrlKey || e.metaKey) {
										e.preventDefault();

										if ("stats" in session.rpcs[uid]) {
											var [menu, innerMenu] = statsMenuCreator();
											printViewStats(innerMenu, uid);
											menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
										}
										e.stopPropagation();
										return false;
									}
								}
							} catch (e) {
								errorlog(e);
							}
						});

						if (session.statsMenu) {
							if ("stats" in session.rpcs[UUID]) {
								if (getById("menuStatsBox")) {
									clearInterval(getById("menuStatsBox").interval);
									getById("menuStatsBox").remove();
								}

								var [menu, innerMenu] = statsMenuCreator();
								printViewStats(innerMenu, UUID);
								menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID);
							}
						}
					} catch (e) {}

					getById("userList").appendChild(insert);

					if (session.rpcs[UUID].videoElement) {
						var volumeBarInsert = document.createElement("input");
						volumeBarInsert.type = "range";
						volumeBarInsert.className = "hidden";
						volumeBarInsert.setAttribute("orient", "vertical");
						volumeBarInsert.max = 100;
						volumeBarInsert.min = 0;
						volumeBarInsert.step = 1;
						volumeBarInsert.dataset.UUID = UUID;
						volumeBarInsert.setAttribute("value", parseFloat(session.rpcs[UUID].videoElement.volume) * 100);
						volumeBarInsert.title = volumeBarInsert.value + "%";

						volumeBarInsert.oninput = function (e) {
							session.rpcs[this.dataset.UUID].videoElement.volume = parseInt(this.value) / 100 || 0;
							if (!session.rpcs[this.dataset.UUID].videoElement.volume) {
								this.parentNode.classList.add("red");
							} else {
								this.parentNode.classList.remove("red");
							}
						};

						volumeBarInsert.onchange = function (e) {
							this.parentNode.querySelector("input").classList.toggle("hidden");
						};

						var volumeButton = document.createElement("i");
						volumeButton.className = "las la-volume-up";
						volumeButton.onclick = function () {
							this.parentNode.querySelector("input").classList.toggle("hidden");
							this.parentNode.volumeBarInsert.focus();
						};

						var volumeInsert = document.createElement("div");
						volumeInsert.className = "volume-control-userlist";
						volumeInsert.volumeBarInsert = volumeBarInsert;
						insert.appendChild(volumeInsert);
						volumeInsert.appendChild(volumeBarInsert);
						volumeInsert.appendChild(volumeButton);
					}

					if (session.rpcs[UUID].remoteMuteState || !session.rpcs[UUID].streamSrc) {
						var muteInsert = document.createElement("div");
						muteInsert.className = "video-mute-state-userlist";
						muteInsert.innerHTML = '<i class="las la-microphone-slash"></i>';
						insert.appendChild(muteInsert);
					} else if (session.rpcs[UUID].voiceMeter) {
						insert.appendChild(session.rpcs[UUID].voiceMeter);
					}
					//getById("userList").innerHTML += "<br />";
					added = true;
				}
			}

			if (!added) {
				getById("connectUsers").style.display = "none";
			} else {
				getById("connectUsers").style.display = "block";
			}
		} catch (e) {}
		updateUserListActive = false;
	}, 200);
}

function resetCanvas() {
	log("resetCanvas();");
	if (!session.streamSrc) {
		checkBasicStreamsExist();
		return;
	}
	session.streamSrc.getVideoTracks().forEach(track => {
		session.canvasSource.width = track.getSettings().width || 1280;
		session.canvasSource.height = track.getSettings().height || 720;
	});
}

function initEffectsImage(){
	if (!session.effectsImage) {
		if (!session.selectedImage_contents) {
			session.selectedImage_contents = getById("selectImage_contents");
		}
		if (session.selectedImage_contents.querySelector("img")) {
			session.effectsImage = session.selectedImage_contents.querySelector("img");
			session.effectsImage.classList.add("selectedContentEffectsImage");
		} else if (session.defaultBackgroundImages && session.defaultBackgroundImages.length) {
			session.effectsImage = document.createElement("img");
			session.effectsImage.onload = function () {
				URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory
			};
			session.effectsImage.src = session.defaultBackgroundImages[0];
			session.effectsImage.classList.add("selectedContentEffectsImage");
		} else {
			session.effectsImage = document.createElement("img");
			session.effectsImage.onload = function () {
				URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory
			};
			session.effectsImage.src = "./media/bg_sample.webp";
		}
	}
}

var LaunchTFWorkerCallback = false;
function TFLiteWorker() {
	if (session.tfliteModule == false) {
		LaunchTFWorkerCallback = true;
		return;
	}
	if (TFLITELOADING) {
		LaunchTFWorkerCallback = true;
		return;
	}
	LaunchTFWorkerCallback = false;
	log("TFLiteWorker() called");


	initEffectsImage();
	//if (session.tfliteModule.looping){return;}

	const segmentationWidth = 256;
	const segmentationHeight = 144;
	const segmentationPixelCount = segmentationWidth * segmentationHeight;
	const inputMemoryOffset = session.tfliteModule._getInputMemoryOffset() / 4;
	const outputMemoryOffset = session.tfliteModule._getOutputMemoryOffset() / 4;
	const segmentationMask = new ImageData(segmentationWidth, segmentationHeight);
	const segmentationMaskCanvas = document.createElement("canvas");
	segmentationMaskCanvas.width = segmentationWidth;
	segmentationMaskCanvas.height = segmentationHeight;
	const segmentationMaskCtx = segmentationMaskCanvas.getContext("2d", { alpha: true, willReadFrequently: true });
	session.tfliteModule.nowTime = new Date().getTime();
	session.tfliteModule.offsetTime = 0;

	var slow = 0;
	var slower = false;

	async function process() {
		if (!(session.effect == "3" || session.effect == "4" || session.effect == "5")) {
			//session.tfliteModule.looping=false;
			errorlog("shouldn't happen");
			return;
		}

		if (session.tfliteModule.activelyProcessing) {
			return;
		}
		session.tfliteModule.activelyProcessing = true;

		if (session.mobile) {
			if (screenWidth !== window.innerWidth) {
				screenWidth = window.innerWidth;

				//session.tfliteModule.looping=false;
				session.tfliteModule.activelyProcessing = false;

				setTimeout(function () {
					updateRenderOutpipe();
				}, 200);
				return;
			}
		}

		try {
			segmentationMaskCtx.filter = "none";
			segmentationMaskCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, segmentationWidth, segmentationHeight);

			const imageData = segmentationMaskCtx.getImageData(0, 0, segmentationWidth, segmentationHeight);

			for (let i = 0; i < segmentationPixelCount; i++) {
				session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3] = imageData.data[i * 4] / 255;
				session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 1] = imageData.data[i * 4 + 1] / 255;
				session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 2] = imageData.data[i * 4 + 2] / 255;
			}

			session.tfliteModule._runInference();

			for (let i = 0; i < segmentationPixelCount; i++) {
				const background = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2];
				const person = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2 + 1];
				const shift = Math.max(background, person);
				const backgroundExp = Math.exp(background - shift);
				const personExp = Math.exp(person - shift);
				segmentationMask.data[i * 4 + 3] = Math.min(Math.pow((255 * personExp) / (backgroundExp + personExp), 1.5) - 10, 255); // softmax
			}

			segmentationMaskCtx.putImageData(segmentationMask, 0, 0);

			session.canvasCtx.globalCompositeOperation = "copy";
			if ((session.mobile && !session.flagship) || slower) {
				session.canvasCtx.filter = "blur(4px)";
			} else {
				session.canvasCtx.filter = "blur(8px)";
			}

			session.canvasCtx.drawImage(segmentationMaskCanvas, 0, 0, segmentationWidth, segmentationHeight, 0, 0, session.canvasSource.width, session.canvasSource.height);

			session.canvasCtx.globalCompositeOperation = "source-in";
			session.canvasCtx.filter = "none";
			session.canvasCtx.drawImage(session.canvasSource, 0, 0);

			session.canvasCtx.globalCompositeOperation = "destination-over";

			if (session.effect == "4") {
				// greenscreen
				session.canvasCtx.filter = "none";
				session.canvasCtx.fillStyle = "#0F0";
				session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height);
			} else if (session.effect == "5") {
				session.canvasCtx.filter = "none";
				if (session.effectsImage.complete) {
					try {
						session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height);
					} catch (e) {}
				}
			} else if (session.effect == "3") {
				// BLUR
				if (session.effectValue) {
					session.canvasCtx.filter = "blur(" + parseInt(session.effectValue) * 2 + "px)";
				} else {
					session.canvasCtx.filter = "blur(4px)"; // Does not work on Safari
				}
				session.canvasCtx.drawImage(session.canvasSource, 0, 0);
				session.canvasCtx.filter = "none";
			} else {
				session.tfliteModule.activelyProcessing = false;
				//session.tfliteModule.looping=false;
				return;
			}

			//////
		} catch (e) {
			errorlog(e);
			session.tfliteModule.activelyProcessing = false;
			//session.tfliteModule.looping=false;
			return;
		}
		session.tfliteModule.lastTime = session.tfliteModule.nowTime;
		session.tfliteModule.nowTime = new Date().getTime();

		var time = 30 - (session.tfliteModule.nowTime - session.tfliteModule.lastTime || 0);
		time = time + (session.tfliteModule.offsetTime || 0);
		session.tfliteModule.activelyProcessing = false;

		slow -= 1;

		if (time <= 0) {
			if (time < -40) {
				slow += 1;
				if (slow > 100) {
					slower = true;
				}
			}
			session.tfliteModule.offsetTime = 0;
		} else {
			slow -= 2;
			session.tfliteModule.offsetTime = time || 0;
		}
	}

	async function processiOS() {
		if (!(session.effect == "3" || session.effect == "4" || session.effect == "5")) {
			errorlog("shouldn't happen");
			//session.tfliteModule.looping=false;
			return;
		}
		if (session.tfliteModule.activelyProcessing) {
			return;
		}
		session.tfliteModule.activelyProcessing = true;

		if (screenWidth !== window.innerWidth) {
			screenWidth = window.innerWidth;
			setTimeout(function () {
				updateRenderOutpipe();
			}, 200);
			//session.tfliteModule.looping=false;
			session.tfliteModule.activelyProcessing = false;
			return;
		}

		try {
			segmentationMaskCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, segmentationWidth, segmentationHeight);

			var imageData = segmentationMaskCtx.getImageData(0, 0, segmentationWidth, segmentationHeight);

			for (let i = 0; i < segmentationPixelCount; i++) {
				session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3] = imageData.data[i * 4] / 255;
				session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 1] = imageData.data[i * 4 + 1] / 255;
				session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 2] = imageData.data[i * 4 + 2] / 255;
			}

			session.tfliteModule._runInference();

			for (let i = 0; i < segmentationPixelCount; i++) {
				const background = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2];
				const person = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2 + 1];
				const shift = Math.max(background, person);
				const backgroundExp = Math.exp(background - shift);
				const personExp = Math.exp(person - shift);
				segmentationMask.data[i * 4 + 3] = 255 - (255 * personExp) / (backgroundExp + personExp); // softmax
			}

			segmentationMaskCtx.putImageData(segmentationMask, 0, 0);

			session.canvasCtx.globalCompositeOperation = "copy";
			session.canvasCtx.drawImage(session.canvasSource, 0, 0);

			session.canvasCtx.globalCompositeOperation = "destination-out";
			session.canvasCtx.drawImage(segmentationMaskCanvas, 0, 0, segmentationWidth, segmentationHeight, 0, 0, session.canvasSource.width, session.canvasSource.height);

			session.canvasCtx.globalCompositeOperation = "destination-over";

			if (session.effect == "4") {
				// greenscreen
				session.canvasCtx.fillStyle = "#0F0";
				session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height);
			} else if (session.effect == "5") {
				if (session.effectsImage.complete) {
					try {
						session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height);
					} catch (e) {}
				}
			} else if (session.effect == "3") {
				// BLUR

				const width = canvasBG.width;
				const height = canvasBG.height;
				ctxBG.drawImage(session.canvasSource, 0, 0, width, height);
				imageData = ctxBG.getImageData(0, 0, width, height);

				const { data } = imageData;

				// THE BELOW BLUR CODE polyfil is by David Enke
				// MIT License: Copyright (c) 2019
				// https://github.com/steveseguin/context-filter-polyfill/blob/master/src/filters/blur.filter.ts
				const wm = width - 1;
				const hm = height - 1;
				const rad1 = amount + 1;
				const r = [];
				const g = [];
				const b = [];
				//const a = [];

				const vmin = [];
				const vmax = [];

				let iterations = 3; // 1 - 3
				let p, p1, p2;
				while (iterations-- > 0) {
					let yw = 0;
					let yi = 0;

					for (let y = 0; y < height; y++) {
						let rsum = data[yw] * rad1;
						let gsum = data[yw + 1] * rad1;
						let bsum = data[yw + 2] * rad1;

						for (let i = 1; i <= amount; i++) {
							p = yw + ((i > wm ? wm : i) << 2);
							rsum += data[p++];
							gsum += data[p++];
							bsum += data[p++];
						}

						for (let x = 0; x < width; x++) {
							r[yi] = rsum;
							g[yi] = gsum;
							b[yi] = bsum;

							if (y === 0) {
								vmin[x] = ((p = x + rad1) < wm ? p : wm) << 2;
								vmax[x] = (p = x - amount) > 0 ? p << 2 : 0;
							}

							p1 = yw + vmin[x];
							p2 = yw + vmax[x];

							rsum += data[p1++] - data[p2++];
							gsum += data[p1++] - data[p2++];
							bsum += data[p1++] - data[p2++];

							yi++;
						}
						yw += width << 2;
					}

					for (let x = 0; x < width; x++) {
						let yp = x;
						let rsum = r[yp] * rad1;
						let gsum = g[yp] * rad1;
						let bsum = b[yp] * rad1;

						for (let i = 1; i <= amount; i++) {
							yp += i > hm ? 0 : width;
							rsum += r[yp];
							gsum += g[yp];
							bsum += b[yp];
						}

						yi = x << 2;

						for (let y = 0; y < height; y++) {
							data[yi] = (rsum * mulSum) >>> shgSum;
							data[yi + 1] = (gsum * mulSum) >>> shgSum;
							data[yi + 2] = (bsum * mulSum) >>> shgSum;

							if (x === 0) {
								vmin[y] = ((p = y + rad1) < hm ? p : hm) * width;
								vmax[y] = (p = y - amount) > 0 ? p * width : 0;
							}

							p1 = x + vmin[y];
							p2 = x + vmax[y];

							rsum += r[p1] - r[p2];
							gsum += g[p1] - g[p2];
							bsum += b[p1] - b[p2];
							yi += width << 2;
						}
					}
				}
				//////////////  END OF BLUR CODE - MIT LICENCED.
				ctxBG.putImageData(imageData, 0, 0);
				session.canvasCtx.drawImage(canvasBG, 0, 0, width, height, 0, 0, session.canvas.width, session.canvas.height);
			} else {
				session.tfliteModule.activelyProcessing = false;
				//session.tfliteModule.looping=false;
				return;
			}
		} catch (e) {
			session.tfliteModule.activelyProcessing = false;
			//session.tfliteModule.looping=false;
			errorlog(e);
			return;
		}

		session.tfliteModule.lastTime = session.tfliteModule.nowTime;
		session.tfliteModule.nowTime = new Date().getTime();

		var time = 30 - (session.tfliteModule.nowTime - session.tfliteModule.lastTime || 0);
		time = time + (session.tfliteModule.offsetTime || 0);

		slow -= 1;
		if (time <= 0) {
			if (time < -40) {
				slow += 1;
				if (slow > 100) {
					slower = true;
				}
			}
			session.tfliteModule.offsetTime = 0;
		} else {
			slow -= 2;
			session.tfliteModule.offsetTime = time || 0;
		}
		session.tfliteModule.activelyProcessing = false;
	}
	//session.tfliteModule.looping=true;

	var screenWidth = window.innerWidth;

	if (iOS || iPad || SafariVersion) {
		var canvasBG = document.createElement("canvas");
		var ctxBG = canvasBG.getContext("2d", { alpha: false });
		var amount = 1.0;
		var mulTable = [1, 57, 41, 21, 203, 34, 97, 73, 227, 91, 149, 62, 105, 45, 39, 137, 241, 107, 3, 173, 39, 71, 65, 238, 219, 101, 187, 87, 81, 151, 141, 133, 249, 117, 221, 209, 197, 187, 177, 169, 5, 153, 73, 139, 133, 127, 243, 233, 223, 107, 103, 99, 191, 23, 177, 171, 165, 159, 77, 149, 9, 139, 135, 131, 253, 245, 119, 231, 224, 109, 211, 103, 25, 195, 189, 23, 45, 175, 171, 83, 81, 79, 155, 151, 147, 9, 141, 137, 67, 131, 129, 251, 123, 30, 235, 115, 113, 221, 217, 53, 13, 51, 50, 49, 193, 189, 185, 91, 179, 175, 43, 169, 83, 163, 5, 79, 155, 19, 75, 147, 145, 143, 35, 69, 17, 67, 33, 65, 255, 251, 247, 243, 239, 59, 29, 229, 113, 111, 219, 27, 213, 105, 207, 51, 201, 199, 49, 193, 191, 47, 93, 183, 181, 179, 11, 87, 43, 85, 167, 165, 163, 161, 159, 157, 155, 77, 19, 75, 37, 73, 145, 143, 141, 35, 138, 137, 135, 67, 33, 131, 129, 255, 63, 250, 247, 61, 121, 239, 237, 117, 29, 229, 227, 225, 111, 55, 109, 216, 213, 211, 209, 207, 205, 203, 201, 199, 197, 195, 193, 48, 190, 47, 93, 185, 183, 181, 179, 178, 176, 175, 173, 171, 85, 21, 167, 165, 41, 163, 161, 5, 79, 157, 78, 154, 153, 19, 75, 149, 74, 147, 73, 144, 143, 71, 141, 140, 139, 137, 17, 135, 134, 133, 66, 131, 65, 129, 1];
		var mulSum = mulTable[amount];
		var shgTable = [0, 9, 10, 10, 14, 12, 14, 14, 16, 15, 16, 15, 16, 15, 15, 17, 18, 17, 12, 18, 16, 17, 17, 19, 19, 18, 19, 18, 18, 19, 19, 19, 20, 19, 20, 20, 20, 20, 20, 20, 15, 20, 19, 20, 20, 20, 21, 21, 21, 20, 20, 20, 21, 18, 21, 21, 21, 21, 20, 21, 17, 21, 21, 21, 22, 22, 21, 22, 22, 21, 22, 21, 19, 22, 22, 19, 20, 22, 22, 21, 21, 21, 22, 22, 22, 18, 22, 22, 21, 22, 22, 23, 22, 20, 23, 22, 22, 23, 23, 21, 19, 21, 21, 21, 23, 23, 23, 22, 23, 23, 21, 23, 22, 23, 18, 22, 23, 20, 22, 23, 23, 23, 21, 22, 20, 22, 21, 22, 24, 24, 24, 24, 24, 22, 21, 24, 23, 23, 24, 21, 24, 23, 24, 22, 24, 24, 22, 24, 24, 22, 23, 24, 24, 24, 20, 23, 22, 23, 24, 24, 24, 24, 24, 24, 24, 23, 21, 23, 22, 23, 24, 24, 24, 22, 24, 24, 24, 23, 22, 24, 24, 25, 23, 25, 25, 23, 24, 25, 25, 24, 22, 25, 25, 25, 24, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 25, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 22, 25, 25, 23, 25, 25, 20, 24, 25, 24, 25, 25, 22, 24, 25, 24, 25, 24, 25, 25, 24, 25, 25, 25, 25, 22, 25, 25, 25, 24, 25, 24, 25, 18];
		var shgSum = shgTable[amount];

		log("session.canvas: " + session.canvas.width + "x" + session.canvas.height);
		canvasBG.width = parseInt(session.canvas.width / 12);
		canvasBG.height = parseInt(session.canvas.height / 12);
		ctxBG.width = canvasBG.width;
		ctxBG.height = canvasBG.height;
		try {
			session.tfliteModule.stopOscillator = setupOscillator(processiOS, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
		} catch (e) {
			errorlog(e);
			session.tfliteModule.stopOscillator = setupOscillator(processiOS, 30);
		}
	} else {
		try {
			session.tfliteModule.stopOscillator = setupOscillator(process, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
		} catch (e) {
			errorlog(e);
			session.tfliteModule.stopOscillator = setupOscillator(process, 30);
		}
	}
}

var insertableStreamWorker = null;

function setupSenderTransform(sender, UUID = false) {
	if (!insertableStreamWorker) {
		insertableStreamWorker = new Worker("./insertableStreamWorker.js", { name: "Insertable Stream worker" });
		insertableStreamWorker.onmessage = event => {
			if (event.data === "insertableStreamWorkerLoaded") {
				if (session.encodedInsertableStreams == "e2ee") {
					if (session.password) {
						insertableStreamWorker.postMessage({ cryptoPhrase: session.password + session.salt + "aDdedSaLt123" }); // salt ⚔️ rainbow
					} else {
						insertableStreamWorker.postMessage({ cryptoKey: "aabbccddeeff00112233445566778899" });
					}
				} else if (session.encodedInsertableStreams == "lyra") {
					insertableStreamWorker.postMessage({
						lyraCodecModule: session.lyraCodecModule
					});
				}
			} else {
				console.log(event.data);
			}
		};
	}

	try {
		const senderStreams = sender.createEncodedStreams();
		const { readable, writable } = senderStreams;
		let operation = "pass";

		if (session.encodedInsertableStreams == "e2ee") {
			operation = "encode";
		} else if (session.encodedInsertableStreams == "red") {
			operation = "redencode";
		} else if (session.encodedInsertableStreams == "lyra" && sender.track && sender.track.kind === "audio") {
			operation = "lyraencode";
		} else if (UUID && session.pcs[UUID] && session.pcs[UUID].preferAudioCodec == "lyra" && sender.track && sender.track.kind === "audio") {
			operation = "lyraencode";
		}

		insertableStreamWorker.postMessage(
			{
				operation: operation,
				readable,
				writable
			},
			[readable, writable]
		);
	} catch (e) {
		errorlog(e);
	}
}

function setupReceiverTransform(receiver, UUID = false) {
	if (!insertableStreamWorker) {
		insertableStreamWorker = new Worker("./insertableStreamWorker.js", { name: "Insertable Stream worker" });
		insertableStreamWorker.onmessage = event => {
			if (event.data === "insertableStreamWorkerLoaded") {
				if (session.encodedInsertableStreams == "e2ee") {
					if (session.password) {
						insertableStreamWorker.postMessage({ cryptoPhrase: session.password + session.salt + "aDdedSaLt123" }); // salt ⚔️ rainbow
					} else {
						insertableStreamWorker.postMessage({ cryptoKey: "aabbccddeeff00112233445566778899" });
					}
				} else if (session.encodedInsertableStreams == "lyra") {
					insertableStreamWorker.postMessage({
						lyraCodecModule: session.lyraCodecModule
					});
				}
			} else {
				console.log(event.data);
			}
		};
	}

	try {
		let operation = "pass";

		if (session.encodedInsertableStreams == "e2ee") {
			operation = "decode";
		} else if (session.encodedInsertableStreams == "red") {
			operation = "reddecode";
		} else if (session.encodedInsertableStreams == "lyra" && receiver.track && receiver.track.kind === "audio") {
			operation = "lyradecode";
		}

		const receiverStreams = receiver.createEncodedStreams();
		const { readable, writable } = receiverStreams;

		insertableStreamWorker.postMessage(
			{
				operation: operation,
				readable,
				writable
			},
			[readable, writable]
		);
	} catch (e) {
		errorlog(e);
	}
}

function mainMeshMask() {
	if (session.TFJSModel === null || session.TFJSModel === true) {
		setTimeout(function () {
			mainMeshMask();
		}, 1000);
		return;
	}
	function heatMapColorforValue(value) {
		var h = parseInt((1.0 - value) * 240);
		if (h < 0) {
			h = 0;
		}
		if (h > 240) {
			h = 240;
		}
		return "hsl(" + h + ", 100%, 50%)";
	}
	async function process() {
		if (session.TFJSModel.activelyProcessing) {
			return;
		}
		session.TFJSModel.activelyProcessing = true;

		if (session.effect !== "6") {
			if (session.TFJSModel.timeoutDraw) {
				session.TFJSModel.timeoutDraw();
				session.TFJSModel.timeoutDraw = null;
			}
			session.TFJSModel.activelyProcessing = false;
			return;
		}

		const predictions = await session.TFJSModel.estimateFaces({
			input: session.canvasSource
		});

		var output = [];
		if (predictions.length > 0) {
			for (let j = 0; j < predictions.length; j++) {
				const fp = predictions[j].annotations;
				session.canvasCtx.fillStyle = "#000000";
				session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height);
				const keypoints = predictions[j].scaledMesh;
				for (let i = 0; i < keypoints.length; i++) {
					var [x, y, z] = keypoints[i];
					x = parseInt(x);
					y = parseInt(y);
					z = parseInt(z);
					if (session.pushEffectsData) {
						output.push(x);
						output.push(y);
					}
					session.canvasCtx.fillStyle = heatMapColorforValue((z + 40) / 60);
					session.canvasCtx.fillRect(x, y, 5, 5);
				}
			}
		}

		if (session.pushEffectsData) {
			//output = FastIntegerCompression.compress(output);
			//log(output);
			if (isIFrame) {
				parent.postMessage(
					{
						effectsData: output,
						eID: session.pushEffectsData
					},
					session.iframetarget
				);
			} else {
				for (var i in session.pcs) {
					if (!session.pcs[i].sendChannel.bufferedAmount) {
						// don't overload things.
						session.sendMessage({ effectsData: output, eID: session.effect }, i);
					}
				}
			}
		}

		if (!session.TFJSModel.timeoutDraw) {
			try {
				session.TFJSModel.timeoutDraw = setupOscillator(process, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
			} catch (e) {
				session.TFJSModel.timeoutDraw = setupOscillator(process, 30); // setTimeout(function(){draw();},33);
			}
		}
		session.TFJSModel.activelyProcessing = false;
	}
	process();
}

var faceDetector = false;
var faceAlignment = false;
var activeDetection = false;

function drawFace() {
	if (session.effect !== "1") {
		return;
	}
	if (faceAlignment) {
		faceAlignment();
		return;
	} else if (faceAlignment === null) {
		return;
	}
	faceAlignment = null;

	var timers = {};
	timers.activelyProcessingDraw = false;

	var ctx = session.canvasCtx;

	function fde1() {
		warnlog("LOADED drawFace()");

		var lastFace = {};

		//session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
		//session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;

		lastFace.x = session.canvasSource.width / 2;
		lastFace.y = session.canvasSource.height / 2;
		lastFace.w = session.canvasSource.width;
		lastFace.h = session.canvasSource.height;

		session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
		session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

		function detectFace() {
			if (activeDetection) {
				return;
			}
			activeDetection = true;
			if (session.effect !== "1") {
				return;
			}
			try {
				faceDetector
					.detect(session.canvasSource)
					.then(faces => {
						if (faces.length) {
							for (let face of faces) {
								lastFace.x = face.boundingBox.x;
								lastFace.y = face.boundingBox.y;
								lastFace.w = face.boundingBox.width;
								lastFace.h = face.boundingBox.height;
								break;
							}
						}
						//setTimeout(function(){draw();},0);
					})
					.catch(e => {
						errorlog("Boo, Face Detection failed: " + e);
					});
			} catch (e) {}
			setTimeout(function () {
				detectFace();
			}, 200);
			activeDetection = false;
		}

		var wh = null;
		var xa = null;
		var ya = null;

		function draw() {
			if (timers.activelyProcessingDraw) {
				return;
			}
			timers.activelyProcessingDraw = true;

			if (session.effect !== "1") {
				timers.activelyProcessingDraw = false;
				if (timers.timeoutDraw) {
					timers.timeoutDraw();
					timers.timeoutDraw = null;
				}
				return;
			}

			try {
				if (!session.canvasSource.width) {
					timers.activelyProcessingDraw = false;
					return;
				}
				if (wh === null && session.canvasSource.width) {
					wh = Math.pow((session.canvasSource.width * session.canvasSource.width) / 36, 0.5);

					xa = 0;
					ya = 0;
				}

				session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
				session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

				if (lastFace.w) {
					wh = wh * 0.999 + Math.pow(lastFace.w * lastFace.h, 0.5) * 0.001;

					var w = wh * 6;

					if (w > session.canvasSource.width) {
						w = session.canvasSource.width;
					}
					if (session.canvasSource.height > session.canvasSource.width) {
						if (w > session.canvasSource.height && session.canvasSource.height > session.canvasSource.width) {
							w = session.canvasSource.height;
						}
					} else if (w > session.canvasSource.width) {
						w = session.canvasSource.width;
					}

					var h = (w / session.canvasSource.width) * session.canvasSource.height;

					xa = xa * 0.998 + 0.002 * (lastFace.x + lastFace.w / 2);
					ya = ya * 0.998 + 0.002 * (lastFace.y + lastFace.h / 2);

					var x = xa - w / 2;
					var y = ya - h / 2;

					if (x < 0) {
						x = 0;
					}
					if (y < 0) {
						y = 0;
					}

					if (x > session.canvasSource.width - w) {
						x = session.canvasSource.width - w;
					}
					if (y > session.canvasSource.height - h) {
						y = session.canvasSource.height - h;
					}

					if (x < 0) {
						x = 0;
					}
					if (y < 0) {
						y = 0;
					}
				}
				//console.log(x, y, w, h, session.canvasSource.width, session.canvasSource.height);
				ctx.drawImage(session.canvasSource, x, y, w, h, 0, 0, session.canvasSource.width, session.canvasSource.height);
				//ctx.beginPath();
				//ctx.rect(lastFace.x, lastFace.y, lastFace.w, lastFace.h);
				//	ctx.stroke();
			} catch (e) {}

			if (!timers.timeoutDraw) {
				try {
					timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
				} catch (e) {
					timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
				}
			} else {
				var res = timers.timeoutDraw("check");
				if (res) {
					try {
						timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
					} catch (e) {
						timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
					}
				}
			}
			timers.activelyProcessingDraw = false;
		}

		if (window.FaceDetector == undefined) {
			if (!session.cleanOutput) {
				warnUser("Face Detection API not detected.\n\nYou may be able to enable it here: chrome://flags/#enable-experimental-web-platform-features");
			}
			faceDetector = false;
		} else {
			faceDetector = new FaceDetector();
		}

		function fde2() {
			if (!timers.activelyProcessingDraw) {
				draw();
			}
			if (!activeDetection) {
				detectFace();
			}
		}
		fde2();
		return fde2;
	}
	faceAlignment = fde1();
}
////////  END CANVAS EFFECTS  ///////////////////

var getFacesActive = false;
async function getFaces() {
	if (getFacesActive) {
		return;
	}
	getFacesActive = true;

	if (session.grabFaceData) {
		if (!faceDetector) {
			if (window.FaceDetector == undefined) {
				if (!session.cleanOutput) {
					warnUser("Face Detection API not detected.\n\nYou may be able to enable it here: chrome://flags/#enable-experimental-web-platform-features");
				}
				session.grabFaceData = false;
				faceDetector = false;

				getFacesActive = false;
				return;
			} else {
				session.grabFaceData = 1;
				faceDetector = new FaceDetector();
			}
		}
		try {
			var videos = {};
			for (var UUID in session.rpcs) {
				if (session.rpcs[UUID].videoElement) {
					await faceDetector
						.detect(session.rpcs[UUID].videoElement)
						.then(faces => {
							videos[session.rpcs[UUID].streamID] = {};
							videos[session.rpcs[UUID].streamID].videoWidth = session.rpcs[UUID].videoElement.videoWidth;
							videos[session.rpcs[UUID].streamID].videoHeight = session.rpcs[UUID].videoElement.videoHeight;
							videos[session.rpcs[UUID].streamID].faces = faces;
						})
						.catch(e => {
							//errorlog("Boo, Face Detection failed: " + e);
						});
				}
			}
			log(videos);
		} catch (e) {}
		pokeIframeAPI("face-tracking-data", videos);
		setTimeout(function () {
			getFaces();
		}, 200);
	}

	getFacesActive = false;
}

//////

var simpleDrawMain = false;
function simpleDraw(reinit = false) {
	let supported = ["8", "overlay"];
	if (!supported.includes(session.effect)) {
		errorlog("not a valid effeect?");
		return;
	}
	if (simpleDrawMain) {
		simpleDrawMain(reinit);
		return;
	} else if (simpleDrawMain === null) {
		return;
	}
	simpleDrawMain = null;

	var timers = {};
	timers.activelyProcessingDraw = false;

	function fde1() {
		try {
			log("LOADED simpleDraw()");

			session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
			session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

			function draw() {
				if (timers.activelyProcessingDraw) {
					return;
				}
				timers.activelyProcessingDraw = true;
				if (!supported.includes(session.effect)) {
					if (timers.timeoutDraw) {
						timers.timeoutDraw();
						timers.timeoutDraw = null;
					}
					timers.activelyProcessingDraw = false;
					return;
				}
				try {
					if (!session.canvasSource.width) {
						timers.activelyProcessingDraw = false;
						return;
					}

					session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
					session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

					session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, session.canvasSource.width, session.canvasSource.height);

					if (session.effect === "overlay" && session.foregroundImg && session.foregroundImg.complete) {
						session.canvasCtx.drawImage(session.foregroundImg, 0, 0, session.canvas.width, session.canvas.height);
					}
				} catch (e) {
					errorlog(e);
				}

				if (!timers.timeoutDraw) {
					try {
						timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
					} catch (e) {
						timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
					}
				} else {
					var res = timers.timeoutDraw("check");
					if (res) {
						try {
							timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
						} catch (e) {
							timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
						}
					}
				}
				timers.activelyProcessingDraw = false;
			}
		} catch (e) {
			errorlog(e);
			timers.activelyProcessingDraw = false;
		}

		function fde2(reinit = false) {
			if (reinit) {
				if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) {
					session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
					session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
				}
			}
			if (!timers.activelyProcessingDraw) {
				draw();
			}
		}
		fde2();

		return fde2;
	}
	simpleDrawMain = fde1();
}
////////  END CANVAS EFFECTS  ///////////////////
//////
function changeZoomPosition(event, ele) {
    const value = parseFloat(ele.value);
    
    // Determine which slider was moved and update its pair
    if (ele.id === "zoomPositionX1" || ele.id === "zoomPositionX") {
        xPosition = value;
        // Update other horizontal slider
        const otherSlider = ele.id === "zoomPositionX1" ? 
            getById("zoomPositionX") : 
            getById("zoomPositionX1");
        if (otherSlider) {
            otherSlider.value = value;
        }
    } else if (ele.id === "zoomPositionY1" || ele.id === "zoomPositionY") {
        yPosition = value;
        // Update other vertical slider
        const otherSlider = ele.id === "zoomPositionY1" ? 
            getById("zoomPositionY") : 
            getById("zoomPositionY1");
        if (otherSlider) {
            otherSlider.value = value;
        }
    }
}

var xPosition = 0.5; // Center position horizontally (0 to 1)
var yPosition = 0.5; // Center position vertically (0 to 1)
var digitalZoomMain = false;
function digitalZoom(resetZoom = false) {
	if (session.effect !== "7") {
		return;
	}
	if (digitalZoomMain) {
		digitalZoomMain(resetZoom);
		return;
	} else if (digitalZoomMain === null) {
		return;
	}
	digitalZoomMain = null;

	var activelyProcessingDraw = false;

	function fde1() {
		try {
			warnlog("LOADED digitalZoom()");

			session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
			session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

			var xa = null;
			var ya = null;
			var zz = 1;

			// Modify the draw function to use the position values
			function draw() {
				if (activelyProcessingDraw) {
					return;
				}
				activelyProcessingDraw = true;
				
				if (session.effect !== "7") {
					zz = 1.0;
					xa = 0;
					ya = 0;
					activelyProcessingDraw = false;
					return;
				}
				
				try {
					if (!session.canvasSource || !session.canvasSource.width) {
						activelyProcessingDraw = false;
						return;
					}
					
					session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
					session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
					
					if (session.effectValue) {
						// Smooth out the zoom factor
						zz = 0.9 * zz + session.effectValue * 0.1;
						
						// Calculate the scaled dimensions
						const scaledWidth = session.canvasSource.width / zz;
						const scaledHeight = session.canvasSource.height / zz;
						
						// Calculate the offset based on position sliders
						// This centers the zoom on the selected position
						xa = (session.canvasSource.width - scaledWidth) * xPosition;
						ya = (session.canvasSource.height - scaledHeight) * yPosition;
						
						// Draw the zoomed region
						session.canvasCtx.drawImage(
							session.canvasSource,
							xa, ya,
							scaledWidth,
							scaledHeight,
							0, 0,
							session.canvasSource.width,
							session.canvasSource.height
						);
					} else {
						// If no zoom, draw the full image
						session.canvasCtx.drawImage(
							session.canvasSource,
							0, 0,
							session.canvasSource.width,
							session.canvasSource.height,
							0, 0,
							session.canvasSource.width,
							session.canvasSource.height
						);
					}
				} catch (e) {
					errorlog(e);
				}
				activelyProcessingDraw = false;
			}
		} catch (e) {
			errorlog(e);
			activelyProcessingDraw = false;
		}

		function fde2(resetZoom = false) {
			if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) {
				session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
				session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
			}

			if (resetZoom) {
				xa = null;
				ya = null;
				zz = 1;
			}

			try {
				setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
			} catch (e) {
				setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
			}

			draw();
		}

		fde2();

		return fde2;
	}
	digitalZoomMain = fde1();
	digitalZoomMain(resetZoom);
}

function rgbToHsv(r, g, b) {
    r /= 255; g /= 255; b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    let h, s, v = max;
    const d = max - min;
    s = max === 0 ? 0 : d / max;
    
    if (max === min) {
        h = 0;
    } else {
        switch (max) {
            case r: h = (g - b) / d + (g < b ? 6 : 0); break;
            case g: h = (b - r) / d + 2; break;
            case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
    }
    return [h, s, v];
}

// real green scree filter
var chromaKeyMain = false;
function chromaKey(reinit = false) {
    let supported = ["14", "15"];
    if (!supported.includes(session.effect)) {
        warnlog("not a valid effect");
        return;
    }
    if (chromaKeyMain) {
        chromaKeyMain(reinit);
        return;
    } else if (chromaKeyMain === null) {
        return;
    }
    chromaKeyMain = null;
    var timers = {};
    timers.activelyProcessingDraw = false;

    function fde1() {
        try {
            log("LOADED chromaKey()");
            session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
            session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);

            function processChromaKey() {
                if (timers.activelyProcessingDraw) return;
                timers.activelyProcessingDraw = true;

                if (!supported.includes(session.effect)) {
                    if (timers.timeoutDraw) {
                        timers.timeoutDraw();
                        timers.timeoutDraw = null;
                    }
                    timers.activelyProcessingDraw = false;
                    return;
                }

                try {
                    if (!session.canvasSource.width) {
                        timers.activelyProcessingDraw = false;
                        return;
                    }

					session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
					session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
					session.canvasCtx.drawImage(session.canvasSource, 0, 0);

					const imageData = session.canvasCtx.getImageData(0, 0, session.canvas.width, session.canvas.height);
					const data = imageData.data;

					const threshold = parseInt(session.effectValue) || 80;
					const smoothing = 15;

					// Green range in HSV (normalized to 0-1)
					const targetHue = 0.33;  // Pure green
					const hueRange = 0.15;   // Range around pure green

					for (let i = 0; i < data.length; i += 4) {
						const r = data[i];
						const g = data[i + 1];
						const b = data[i + 2];
						
						// Quick pre-check for obvious non-green pixels
						if (g <= r || g <= b) {
							continue;
						}
						
						const [h, s, v] = rgbToHsv(r, g, b);
						
						// Calculate how "green" the pixel is
						const hueDiff = Math.abs(h - targetHue);
						const isInGreenRange = hueDiff <= hueRange || hueDiff >= (1 - hueRange);
						
						if (!isInGreenRange) {
							continue;
						}
						
						// Green intensity calculation
						const greenDominance = (g - Math.max(r, b)) / 255;
						const saturationBoost = s * 0.7;  // Reduce impact of washed-out greens
						
						const keyValue = (greenDominance * 0.6 + saturationBoost * 0.4) * 100;
						
						let alpha = 255;
						if (keyValue > threshold) {
							const smoothFactor = Math.min((keyValue - threshold) / smoothing, 1);
							alpha = (1 - smoothFactor) * 255;
						}
						
						data[i + 3] = alpha;
					}

					session.canvasCtx.putImageData(imageData, 0, 0);

					if (session.effect === "15" && session.effectsImage && session.effectsImage.complete) {
						session.canvasCtx.globalCompositeOperation = 'destination-over';
						session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height);
						session.canvasCtx.globalCompositeOperation = 'source-over';
					}
                } catch (e) {
                    errorlog(e);
                }

                if (!timers.timeoutDraw) {
                    try {
                        timers.timeoutDraw = setupOscillator(processChromaKey, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
                    } catch (e) {
                        timers.timeoutDraw = setupOscillator(processChromaKey, 40);
                    }
                } else {
                    var res = timers.timeoutDraw("check");
                    if (res) {
                        try {
                            timers.timeoutDraw = setupOscillator(processChromaKey, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
                        } catch (e) {
                            timers.timeoutDraw = setupOscillator(processChromaKey, 40);
                        }
                    }
                }
                timers.activelyProcessingDraw = false;
            }
        } catch (e) {
            errorlog(e);
            timers.activelyProcessingDraw = false;
        }

        function fde2(reinit = false) {
            if (reinit) {
                if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) {
                    session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
                    session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
                }
            }
            if (!timers.activelyProcessingDraw) {
                processChromaKey();
            }
        }
        fde2();
        return fde2;
    }
    chromaKeyMain = fde1();
}
////////  END CANVAS EFFECTS  ///////////////////

function getNativeOutputResolution() {
	try {
		if (session.videoElement && session.videoElement.srcObject) {
			var tracks = session.videoElement.srcObject.getVideoTracks();
			if (tracks.length && tracks[0].getSettings) {
				return tracks[0].getSettings();
			} else {
				return false;
			}
		} else if (session.videoElement && session.videoElement.videoWidth && session.videoElement.videoHeight) {
			return { width: session.videoElement.videoWidth, height: session.videoElement.videoHeight };
		}
	} catch (e) {
		return false;
	}
}

function toggleSceneStats(button) {
	var UUID = button.dataset.UUID;

	if (button.value == 1) {
		button.value = 0;
		button.classList.remove("pressed");
		button.ariaPressed = "false";
		if (UUID) {
			session.rpcs[UUID].allowGraphs = false;
		} else {
			session.allowDirectorGraph = false;
		}
	} else {
		button.value = 1;
		button.classList.add("pressed");
		button.ariaPressed = "true";
		if (UUID) {
			session.rpcs[UUID].allowGraphs = true;
		} else {
			session.allowDirectorGraph = true;
		}
	}

	if (UUID) {
		var controls = getById("container_" + UUID);
	} else {
		var controls = getById("container_director");
	}

	if (button.value == 1) {
		controls.querySelectorAll("[data-no-scenes]").forEach(ele => {
			ele.classList.remove("hidden");
			if (ele.dataset.message) {
				ele.innerHTML = "Requesting data ..";
			}
		});

		if (controls.querySelector('[data-action-type="stats-graphs-bitrate"]')) {
			controls.querySelector('[data-action-type="stats-graphs-bitrate"]').classList.remove("hidden");
		}
		if (controls.querySelector('[data-action-type="stats-graphs-details"]')) {
			controls.querySelector('[data-action-type="stats-graphs-details"]').classList.remove("hidden");
		}
		if (UUID) {
			session.sendRequest({ requestStatsContinuous: true }, UUID);
		}
	} else {
		if (UUID) {
			session.sendRequest({ requestStatsContinuous: false }, UUID);
		}
		if (controls.querySelector('[data-action-type="stats-graphs-bitrate"]')) {
			controls.querySelector('[data-action-type="stats-graphs-bitrate"]').classList.add("hidden");
		}
		if (controls.querySelector('[data-action-type="stats-graphs-details"]')) {
			controls.querySelector('[data-action-type="stats-graphs-details"]').classList.add("hidden");
		}
	}
}
function getColor(value) {
	var hue = (value * 120).toString(10);
	return ["hsl(", hue, ",100%,50%)"].join("");
}

function plotData(info, UUID, uuid) {
	// type = "bitrate" or "nacks"
	log("plot data");

	var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-bitrate"]');

	if (!container) {
		log("container not found");
		return;
	}
	var canvas = getById("container_" + UUID).querySelector('canvas[data-uid="' + uuid + '"]');
	var canvasNew = false;
	if (!canvas) {
		canvasNew = true;
		canvas = document.createElement("canvas");
		canvas.height = 50;
		canvas.width = 124;
		canvas.className = "canvasStats";
		canvas.history_nacks = [];
		canvas.history_bitrate = [];
		canvas.target = 4000;
		if (info.scene) {
			canvas.title = "Scene: " + info.scene + ". Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments.";
		} else if (info.label) {
			canvas.title = "Label: " + info.label + ". Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments";
		} else {
			canvas.title = "Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments";
		}

		canvas.dataset.uid = uuid;
		container.appendChild(canvas);
	}

	selfDestructElement(UUID, uuid);

	var context = canvas.getContext("2d");

	var bitrate = 0;
	if ("video_bitrate_kbps" in info) {
		bitrate = info.video_bitrate_kbps;
	}
	if (isNaN(bitrate)) {
		bitrate = 0;
	}

	if (bitrate < 0) {
		bitrate = 0;
	}

	var nacks = 0;
	if ("nacks_per_second" in info) {
		nacks = info.nacks_per_second;
	}
	if (isNaN(nacks)) {
		nacks = 0;
	}
	if (nacks < 0) {
		nacks = 0;
	}

	var height = context.canvas.height;
	var width = context.canvas.width;

	canvas.history_nacks.push(nacks);
	canvas.history_bitrate.push(bitrate);

	canvas.history_nacks = canvas.history_nacks.slice(-125);
	canvas.history_bitrate = canvas.history_bitrate.slice(-125);

	var maxBitrate = Math.max(...canvas.history_bitrate);

	var target = canvas.target || 4000;
	if (target && maxBitrate > target) {
		canvas.target = maxBitrate * 1.5; // set it higher than it needs to be, so it doens't jump around a lot
		var yScale = height / canvas.target;
		context.clearRect(0, 0, width, height);
		var x = width - 1;
		var w = 1;

		for (var i = 0; i < canvas.history_bitrate.length; i++) {
			var nacks = canvas.history_nacks[i];
			var bitrate = canvas.history_bitrate[i];

			var val = (10 - nacks) / 10;
			if (val > 1) {
				val = 1;
			} else if (val < 0) {
				val = 0;
			}
			var color = getColor(val);
			var y = height - bitrate * yScale;
			context.fillStyle = color;
			context.fillRect(x, y, w, height);
			context.fillStyle = "#DDD5";
			context.fillRect(x, y - 2, w, 4);

			if (y - 5 > 0) {
				context.fillStyle = "#FFF3";
				context.fillRect(x, y + 2, w, 1);
			}

			var imageData = context.getImageData(1, 0, width - 1, height);
			context.putImageData(imageData, 0, 0);
			context.clearRect(width - 1, 0, 1, height);
		}

		for (var tt = 2500; tt < canvas.target; tt += 2500) {
			var y = parseInt(height - tt * yScale);
			context.fillStyle = "#0555";
			context.fillRect(0, y, width, 1);
		}
		log("finished plotting a new y-axis");
		return;
	}
	//if (info.available_outgoing_bitrate_kbps){
	// limit target, but requires a history
	//}
	var val = (10 - nacks) / 10;
	if (val > 1) {
		val = 1;
	} else if (val < 0) {
		val = 0;
	}
	var color = getColor(val);

	var yScale = height / target;

	var x = width - 1;
	var y = height - bitrate * yScale;
	var w = 1;

	context.fillStyle = color;
	context.fillRect(x, y, w, height);
	context.fillStyle = "#DDD5";
	context.fillRect(x, y - 2, w, 4);

	if (y - 5 > 0) {
		context.fillStyle = "#FFF3";
		context.fillRect(x, y + 2, w, 1);
	}

	context.fillStyle = "#0555";
	if (canvasNew) {
		for (var tt = 2500; tt < target; tt += 2500) {
			var y = parseInt(height - tt * yScale);
			context.fillRect(0, y, width, 1);
		}
	} else {
		for (var tt = 2500; tt < target; tt += 2500) {
			var y = parseInt(height - tt * yScale);
			context.fillRect(x, y, 1, 1);
		}
	}

	var imageData = context.getImageData(1, 0, width - 1, height);
	context.putImageData(imageData, 0, 0);
	context.clearRect(width - 1, 0, 1, height);

	log("finished plotting");
}

function selfDestructElement(UUID, uid) {
	getById("container_" + UUID)
		.querySelectorAll('[data-uid="' + uid + '"]')
		.forEach(ele => {
			ele.classList.remove("greyout");
			clearTimeout(ele.selfFadeout);
			ele.selfFadeout = setTimeout(
				function (ele) {
					ele.classList.add("greyout");
				},
				4000,
				ele
			);

			clearTimeout(ele.selfDestruct);
			ele.selfDestruct = setTimeout(
				function (ele) {
					ele.remove();
				},
				10000,
				ele
			);
		});
}

function directorGraphStats() {
	if (!(session.allowDirectorGraph || session.allowGraphs)) {
		return;
	}

	if (session.director) {
		var UUID = "director";
		var maincon = getById("container_director");

		var sceneStats = {};
		for (var uuid in session.pcs) {
			if (session.pcs[uuid].scene !== false) {
				sceneStats[uuid] = {};
				sceneStats[uuid].label = session.pcs[uuid].label;
				sceneStats[uuid].scene = session.pcs[uuid].scene;
				sceneStats[uuid].resolution = session.pcs[uuid].stats.resolution;
				sceneStats[uuid].video_bitrate_kbps = session.pcs[uuid].stats.video_bitrate_kbps;
				sceneStats[uuid].video_encoder = session.pcs[uuid].stats.video_encoder;
			}
		}
		if (!Object.keys(sceneStats).length) {
			maincon.querySelectorAll("[data-no-scenes]").forEach(ele => {
				ele.classList.remove("hidden");
				if (ele.dataset.message) {
					ele.innerHTML = "No scenes active";
				}
			});

			log("zero size");
			return;
		}
		maincon.querySelectorAll("[data-no-scenes]").forEach(ele => {
			ele.classList.add("hidden");
		});

		for (var uuid in sceneStats) {
			var container = maincon.querySelector('[data-action-type="stats-graphs-details-container"][data-uid="' + uuid + '"]');
			if (!container) {
				container = maincon.querySelector('[data-action-type="stats-graphs-details-container"]').cloneNode(true);
				container.dataset.uid = uuid;
				container.classList.remove("hidden");
				maincon.querySelector('[data-action-type="stats-graphs-details"]').appendChild(container);
			}
			plotData(sceneStats[uuid], UUID, uuid);
			
			if ("video_bitrate_kbps" in sceneStats[uuid] && sceneStats[uuid].video_bitrate_kbps !== "video_bitrate_kbps") {
				var span = container.querySelector("[data-bitrate]");
				if (span) {
					span.classList.remove("hidden");
					span.innerHTML = "video bitrate: " + parseInt(sceneStats[uuid].video_bitrate_kbps) + " (kbps)";
					span.style.cursor = "pointer";
					span.title = "Click to adjust bitrate";
					span.onclick = async function(e) {
						e.preventDefault();
						e.stopPropagation();
						
						var currentUUID = this.closest('[data-action-type="stats-graphs-details-container"]').dataset.uid;
						
						const result = await promptAlt("Select target bitrate (kbps)", false, false, false, false, false, false, {
							type: 'select',
							options: ['50', '500', '1000', '2000', '5000', '10000', '20000', '[Custom]'],
							placeholder: 'Enter custom bitrate in kbps'
						});
						
						if (result) {
							var msg = {
								targetBitrate: parseInt(result),
								UUID: currentUUID,
								requestAs: uuid
							};
							if (isIFrame) {
                                parent.postMessage(msg, session.iframetarget);
                            }
							session.sendRequest(msg);
						}
					};
				}
			}
			
			var span = container.querySelector("[data-scene-name]");
			if (span && "label" in sceneStats[uuid] && sceneStats[uuid].label) {
				span.classList.remove("hidden");
				span.innerHTML = "stats for viewer: " + sceneStats[uuid].label;
			} else if (span && "scene" in sceneStats[uuid] && sceneStats[uuid].scene !== false) {
				span.classList.remove("hidden");
				span.innerHTML = "stats for scene: " + sceneStats[uuid].scene;
			} else if (uuid === "meshcast") {
				span.classList.remove("hidden");
				span.innerHTML = "stats for meshcast ingest";
				span.title = "You can use &label=xxxx to give your view links a unique label";
			} else {
				span.classList.remove("hidden");
				span.innerHTML = "stats for some viewer";
				span.title = "You can use &label=xxxx to give your view links a unique label";
			}
			
			if ("resolution" in sceneStats[uuid]) {
				var span = container.querySelector("[data-resolution]");
				if (span) {
					span.classList.remove("hidden");
					span.innerHTML = sceneStats[uuid].resolution;
					span.style.cursor = "pointer";
					span.title = "Click to adjust resolution";
					span.onclick = async function(e) {
						e.preventDefault();
						e.stopPropagation();
						
						var currentUUID = this.closest('[data-action-type="stats-graphs-details-container"]').dataset.uid;
						
						const result = await promptAlt("Select target resolution", false, false, false, false, false, false, {
							type: 'select',
							options: ['360', '720', '1080', '[Custom]'],
							placeholder: 'Enter custom height in pixels'
						});
						
						if (result) {
							session.requestResolution(currentUUID, 4096, result || 2160, false, uuid);
						}
					};
				}
			}

			if ("video_encoder" in sceneStats[uuid]) {
				var span = container.querySelector("[data-video-codec]");
				if (span) {
					span.classList.remove("hidden");
					span.innerHTML = "video codec: " + sceneStats[uuid].video_encoder;
				}
			}
		}
	}
}

function remoteStats(msg, UUID) {
    if (isIFrame) {
        parent.postMessage({ remoteStats: msg.remoteStats, streamID: session.rpcs[UUID].streamID, UUID: UUID }, session.iframetarget);
    }

    if (!(session.rpcs[UUID].allowGraphs || session.allowGraphs)) {
        return;
    }

    if (session.director) {
        var size = 0;
        for (var key in msg.remoteStats) {
            if (msg.remoteStats.hasOwnProperty(key)) {
                size++;
            }
        }

        if (!size) {
            getById("container_" + UUID)
                .querySelectorAll("[data-no-scenes]")
                .forEach(ele => {
                    ele.classList.remove("hidden");
                    if (ele.dataset.message) {
                        ele.innerHTML = "No scenes active";
                    }
                });
            log("zero size");
            return;
        }

        getById("container_" + UUID)
            .querySelectorAll("[data-no-scenes]")
            .forEach(ele => {
                ele.classList.add("hidden");
            });

        for (var uuid in msg.remoteStats) {
            var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-details-container"][data-uid="' + uuid + '"]');
            if (!container) {
                container = getById("container_" + UUID)
                    .querySelector('[data-action-type="stats-graphs-details-container"]')
                    .cloneNode(true);
                container.dataset.uid = uuid;
                container.classList.remove("hidden");
                getById("container_" + UUID)
                    .querySelector('[data-action-type="stats-graphs-details"]')
                    .appendChild(container);
            }

            plotData(msg.remoteStats[uuid], UUID, uuid);

            if ("video_bitrate_kbps" in msg.remoteStats[uuid] && msg.remoteStats[uuid].video_bitrate_kbps !== "video_bitrate_kbps") {
                var span = container.querySelector("[data-bitrate]");
                if (span) {
                    span.classList.remove("hidden");
                    span.innerHTML = "video bitrate: " + parseInt(msg.remoteStats[uuid].video_bitrate_kbps) + " (kbps)";
                    span.style.cursor = "pointer";
                    span.title = "Click to adjust bitrate";
                    span.onclick = async function(e) {
                        e.preventDefault();
                        e.stopPropagation();

                        const result = await promptAlt("Select target bitrate (kbps)", false, false, false, false, false, false, {
                            type: 'select',
                            options: ['50', '500', '1000', '2000', '5000', '10000', '20000', '[Custom]'],
                            placeholder: 'Enter custom bitrate in kbps'
                        });

                        if (result) {
                            var msg = {
                                targetBitrate: parseInt(result),
                                UUID: UUID,
                                requestAs: uuid
                            };
                            if (isIFrame) {
                                parent.postMessage(msg, session.iframetarget);
                            } 
							session.sendRequest(msg);
                        }
                    };
                }
            }

            var span = container.querySelector("[data-scene-name]");
            if (span && "label" in msg.remoteStats[uuid] && msg.remoteStats[uuid].label) {
                span.classList.remove("hidden");
                span.innerHTML = "stats for viewer: " + msg.remoteStats[uuid].label;
            } else if (span && "scene" in msg.remoteStats[uuid] && msg.remoteStats[uuid].scene !== false) {
                span.classList.remove("hidden");
                span.innerHTML = "stats for scene: " + msg.remoteStats[uuid].scene;
            } else if (uuid === "meshcast") {
                span.classList.remove("hidden");
                span.innerHTML = "stats for meshcast ingest";
                span.title = "You can use &label=xxxx to give your view links a unique label";
            } else {
                span.classList.remove("hidden");
                span.innerHTML = "stats for some viewer";
                span.title = "You can use &label=xxxx to give your view links a unique label";
            }

            if ("resolution" in msg.remoteStats[uuid]) {
                var span = container.querySelector("[data-resolution]");
                if (span) {
                    span.classList.remove("hidden");
                    span.innerHTML = msg.remoteStats[uuid].resolution;
                    span.style.cursor = "pointer";
                    span.title = "Click to adjust resolution";
                    span.onclick = async function(e) {
                        e.preventDefault();
                        e.stopPropagation();
						
                        const result = await promptAlt("Select target resolution", false, false, false, false, false, false, {
                            type: 'select',
                            options: ['360', '720', '1080', '1440', '2160', '[Custom]'],
                            placeholder: 'Enter custom height in pixels'
                        });

                        if (result) {
                            session.requestResolution(UUID, 4096, result || 2160, false, uuid);
                        }
                    };
                }
            }

            if ("video_encoder" in msg.remoteStats[uuid]) {
                var span = container.querySelector("[data-video-codec]");
                if (span) {
                    span.classList.remove("hidden");
                    span.innerHTML = "video codec: " + msg.remoteStats[uuid].video_encoder;
                }
            }
        }
    }
}

function processStats(UUID) {
	// for (pc in session.pcs){session.pcs[pc].getStats().then(function(stats) {stats.forEach(stat=>{if (stat.id.includes("RTCIce")){console.log(stat)}})})};

	if (!session.rpcs || !(UUID in session.rpcs)) {
		return;
	}

	try {
		if (session.rpcs[UUID].videoElement.paused) {
			if (session.firstPlayTriggered) {
				if (session.audioCtx.state == "suspended") {
					//  added oct 9th 2022
					try {
						session.audioCtx.resume();
					} catch (e) {
						warnlog(e);
					}
				}
				if (session.audioCtx.state == "running") {
					// NOTE:  I Don't know why this was
					log("trying to play");
					session.rpcs[UUID].videoElement
						.play()
						.then(_ => {
							log("playing 8");

							//if ((session.audioEffects===true) || session.pushLoudness){
							//	updateIncomingAudioElement(UUID);
							//}
						})
						.catch(warnlog);
				}
			}
		}
	} catch (e) {}

	try {
		if (session.rpcs[UUID].realUUID && session.rpcs[session.rpcs[UUID].realUUID]) {
			var node = session.rpcs[session.rpcs[UUID].realUUID];
		} else {
			var node = session.rpcs[UUID];
		}

		var validTrackIds = [];
		if (session.rpcs[UUID].streamSrc) {
			session.rpcs[UUID].streamSrc.getTracks().forEach(trk => {
				validTrackIds.push(trk.id);
			});
		}

		if (session.rpcs[UUID].whep) {
			processMeshcastStats(UUID);
			if (!node.getStats) {
				clearTimeout(session.rpcs[UUID].getStatsTimeout);
				session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, session.statsInterval, UUID);

				//setTimeout(processStats, session.statsInterval, UUID); // no p2p, so lets do WHEP again manually.
			}
		}

		if (node.getStats) {
			node.getStats().then(function (stats) {
				if (!(UUID in session.rpcs)) {
					return;
				}

				clearTimeout(session.rpcs[UUID].getStatsTimeout);
				session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, session.statsInterval, UUID);

				if (!session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) {
					session.rpcs[UUID].stats["Peer-to-Peer_Connection"] = {};
				}

				var nominatedCandidate = false;
				var candidates = {};
				var ipleakingAllowedRemote = false;
				var ipleakingAllowedLocal = false;

				stats.forEach(stat => {
					try {
						if (stat.id && stat.id.startsWith("DEPRECATED_")) {
							return;
						}

						var trackID = stat.trackIdentifier || stat.id || false;

						if (stat.type == "track" && stat.remoteSource) {
							if (stat.trackIdentifier && !validTrackIds.includes(stat.trackIdentifier)) {
								return;
							}

							if (stat.id in session.rpcs[UUID].stats) {
								session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier;
								session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0;
								session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
								session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;

								if ("frameWidth" in stat) {
									if ("frameHeight" in stat) {
										session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth + " x " + stat.frameHeight;
										session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth;
										session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight;
									}
								}
							} else {
								var media = {};
								media._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
								media._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
								media.Jitter_Buffer_ms = 0;
								media._trackID = stat.trackIdentifier;

								session.rpcs[UUID].stats[stat.id] = media;

								if (stat.kind && stat.kind == "audio") {
									session.rpcs[UUID].stats[stat.id].type = "Audio Track";
									session.rpcs[UUID].stats[stat.id]._type = "audio";
								} else if (stat.kind && stat.kind == "video") {
									session.rpcs[UUID].stats[stat.id].type = "Video Track";
									session.rpcs[UUID].stats[stat.id]._type = "video";
								}
							}
						} else if (stat.type == "remote-candidate") {
							candidates[stat.id] = stat;
							if (stat.candidateType != "relay") {
								ipleakingAllowedRemote = true;
							}
						} else if (stat.type == "local-candidate") {
							candidates[stat.id] = stat;
							if (stat.candidateType != "relay") {
								ipleakingAllowedLocal = true;
							}
						} else if (stat.type == "candidate-pair" && stat.nominated) {
							if (!nominatedCandidate) {
								nominatedCandidate = stat;
							} else if (nominatedCandidate.priority < stat.priority) {
								nominatedCandidate = stat;
							}
						} else if (stat.type == "transport") {
							if ("bytesReceived" in stat) {
								if ("_bytesReceived" in session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) {
									if (session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp) {
										if (stat.timestamp) {
											session.rpcs[UUID].stats["Peer-to-Peer_Connection"].total_recv_bitrate_kbps = parseInt((8 * (stat.bytesReceived - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._bytesReceived)) / (stat.timestamp - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp));
											hideStreamLowBandwidth(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].total_recv_bitrate_kbps, UUID);
											//changeSceneLowBandwidth(session.rpcs[UUID].stats['Peer-to-Peer_Connection'].total_recv_bitrate_kbps, UUID);
										}
									}
								}
								session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._bytesReceived = stat.bytesReceived;
							}
							if ("timestamp" in stat) {
								session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp = stat.timestamp;
								if (!session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart) {
									session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart = stat.timestamp;
								} else {
									session.rpcs[UUID].stats["Peer-to-Peer_Connection"].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart) / 600) / 100;
								}
							}
						} else if (stat.type == "inbound-rtp" && trackID) {
							if (stat.trackIdentifier && !validTrackIds.includes(stat.trackIdentifier)) {
								return;
							}

							session.rpcs[UUID].stats[trackID] = session.rpcs[UUID].stats[trackID] || {};

							if (stat.trackIdentifier) {
								session.rpcs[UUID].stats[trackID]._trackID = stat.trackIdentifier;
							}
							if ("jitterBufferDelay" in stat) {
								session.rpcs[UUID].stats[trackID].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[trackID]._jitter_delay_2)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[trackID]._jitter_count_2)) || 0;
								session.rpcs[UUID].stats[trackID]._jitter_delay_2 = parseFloat(stat.jitterBufferDelay) || 0;
								session.rpcs[UUID].stats[trackID]._jitter_count_2 = parseInt(stat.jitterBufferEmittedCount) || 0;
							}

							if ("frameWidth" in stat) {
								if ("frameHeight" in stat) {
									session.rpcs[UUID].stats[trackID].Resolution = stat.frameWidth + " x " + stat.frameHeight;
									session.rpcs[UUID].stats[trackID]._frameWidth = stat.frameWidth;
									session.rpcs[UUID].stats[trackID]._frameHeight = stat.frameHeight;
								}
							}

							session.rpcs[UUID].stats[trackID].Bitrate_in_kbps = parseInt((8 * (stat.bytesReceived - (session.rpcs[UUID].stats[trackID]._last_bytes || 0))) / (stat.timestamp - session.rpcs[UUID].stats[trackID]._last_time));

							session.rpcs[UUID].stats[trackID]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[trackID]._last_bytes;
							session.rpcs[UUID].stats[trackID]._last_time = stat.timestamp || session.rpcs[UUID].stats[trackID]._last_time;

							if (stat.mediaType == "video") {
								session.rpcs[UUID].stats._codecId = stat.codecId;
								session.rpcs[UUID].stats._codecIdTrackId = trackID;

								session.rpcs[UUID].stats[trackID].type = "Video Stream";
								session.rpcs[UUID].stats[trackID]._type = "video";
								if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP8") {
									session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0;
									session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0;

									log("OBS PLI FIX MODE ON");
									if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix) {
										// heavy packet loss with no pliCount?
										session.requestKeyframe(UUID);
										session.rpcs[UUID].stats[trackID].nackTrigger = 0;
										log("TRYING KEYFRAME");
									} else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) {
										session.rpcs[UUID].stats[trackID].nackTrigger = 0;
									}
								} else if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP9") {
									session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0;
									session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0;

									log("OBS PLI FIX MODE ON");
									if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix * 4) {
										// heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often.
										session.requestKeyframe(UUID);
										session.rpcs[UUID].stats[trackID].nackTrigger = 0;
										log("TRYING KEYFRAME");
									} else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) {
										session.rpcs[UUID].stats[trackID].nackTrigger = 0;
									}
								}

								session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0;
								session.rpcs[UUID].stats[trackID].streamErrors_nackCount = stat.nackCount || 0;

								//warnlog(stat);

								if ("framesPerSecond" in stat) {
									session.rpcs[UUID].stats[trackID].FPS = parseInt(stat.framesPerSecond);
								} else if ("framesDecoded" in stat && stat.timestamp) {
									var lastFramesDecoded = 0;
									var lastTimestamp = 0;
									try {
										lastFramesDecoded = session.rpcs[UUID].stats[trackID]._framesDecoded;
										lastTimestamp = session.rpcs[UUID].stats[trackID]._timestamp;
									} catch (e) {}
									session.rpcs[UUID].stats[trackID].FPS = parseInt((10 * (stat.framesDecoded - lastFramesDecoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10;

									//session.rpcs[UUID].stats[trackID].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp));
									session.rpcs[UUID].stats[trackID]._framesDecoded = stat.framesDecoded;
									session.rpcs[UUID].stats[trackID]._timestamp = stat.timestamp / 1000;
								}
							} else if (stat.mediaType == "audio") {
								//log("AUDIO LEVEL: "+stat.audioLevel);
								session.rpcs[UUID].stats._audioCodecId = stat.codecId;
								session.rpcs[UUID].stats._audioCodecIdTrackId = trackID;
								session.rpcs[UUID].stats[trackID].type = "Audio Stream";
								session.rpcs[UUID].stats[trackID]._type = "audio";
								if ("audioLevel" in stat) {
									session.rpcs[UUID].stats[trackID].audio_level = parseInt(parseFloat(stat.audioLevel) * 10000) / 10000.0;
								}
							}

							if ("packetsLost" in stat && "packetsReceived" in stat) {
								if (!("_packetsLost" in session.rpcs[UUID].stats[trackID])) {
									session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost;
								}
								if (!("_packetsReceived" in session.rpcs[UUID].stats[trackID])) {
									session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived;
								}

								if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[trackID])) {
									session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = 0;
								}

								let packetLoss = ((stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost) * 100.0) / (stat.packetsReceived - session.rpcs[UUID].stats[trackID]._packetsReceived + (stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost)) || 0;

								/* if (session.rpcs[UUID].stats[trackID]._type && (session.rpcs[UUID].stats[trackID]._type =="video")){
									if (packetLoss>1){
										var data = {};
										data.bitrate = parseInt(session.rpcs[UUID].stats[trackID].Bitrate_in_kbps*0.8);
										session.sendRequest(data,UUID);
									} else {
										var data = {};
										data.bitrate = parseInt(session.rpcs[UUID].stats[trackID].Bitrate_in_kbps*1.1);
										session.sendRequest(data,UUID);
									}
								} */

								session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage * 0.35 + 0.65 * packetLoss;

								if (session.rpcs[UUID].signalMeter && session.rpcs[UUID].stats[trackID]._type === "video") {
									if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.01) {
										if (session.rpcs[UUID].stats[trackID].Bitrate_in_kbps == 0) {
											session.rpcs[UUID].signalMeter.dataset.level = 0;
										} else {
											session.rpcs[UUID].signalMeter.dataset.level = 5;
										}
									} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.3) {
										session.rpcs[UUID].signalMeter.dataset.level = 4;
									} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 1.0) {
										session.rpcs[UUID].signalMeter.dataset.level = 3;
									} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 3.5) {
										session.rpcs[UUID].signalMeter.dataset.level = 2;
									} else {
										session.rpcs[UUID].signalMeter.dataset.level = 1;
									}
								}

								session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived;
								session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost;
							}
						} else if ("_codecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._codecId) {
							if ("mimeType" in stat) {
								if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]) {
									session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
								} else {
									session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {};
									session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
								}
							}
							if ("frameHeight" in stat) {
								if ("frameWidth" in stat) {
									session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth) + " x " + parseInt(stat.frameHeight);
								}
							}
						} else if ("_audioCodecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._audioCodecId) {
							if ("mimeType" in stat) {
								var addOnDescription = stat.mimeType;
								addOnDescription = addOnDescription.replace("audio/", "");

								if ("sdpFmtpLine" in stat) {
									if (stat.sdpFmtpLine.includes("useinbandfec=1")) {
										addOnDescription += ", /w fec";
									}
								}

								if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId]) {
								} else {
									session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId] = {};
								}
								session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].codec = addOnDescription;

								if (stat.clockRate) {
									session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].clockRate = stat.clockRate;
									if (stat.channels) {
										session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].clockRate += " / " + stat.channels;
									}
								}
							}
						} else if (Firefox) {
							if ("frameWidth" in stat) {
								session.rpcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight;
								if ("framesPerSecond" in stat) {
									session.rpcs[UUID].stats.resolution += " @ " + stat.framesPerSecond;
								}
							}
							if ("mimeType" in stat && "type" in stat && "id" in stat && stat.type == "codec") {
								if (stat.mimeType.includes("video")) {
									session.rpcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1];
								} else if (stat.mimeType.includes("audio")) {
									session.rpcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1];
									if (stat.clockRate) {
										session.rpcs[UUID].stats.audio_clockRate = stat.clockRate;
										if (stat.channels) {
											session.rpcs[UUID].stats.audio_clockRate += " / " + stat.channels;
										}
									} else if (stat.sdpFmtpLine) {
										session.rpcs[UUID].stats.fmtp = stat.sdpFmtpLine;
									}
								}
							}
							/* if ("jitter" in stat){
								if (("kind" in stat) && (stat.kind=="video")){
									session.rpcs[UUID].stats.video_jitter_ms = parseInt(stat.jitter*1000);
								} else if (("kind" in stat) && (stat.kind=="audio")){
									session.rpcs[UUID].stats.audio_jitter_ms = parseInt(stat.jitter*1000);
								}
							} */
							if ("bytesReceived" in stat) {
								if ("kind" in stat && stat.kind == "video") {
									if ("_bytesReceived_video" in session.rpcs[UUID].stats) {
										session.rpcs[UUID].stats.videoBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_video) / ((1024 * session.statsInterval) / 8000));
									}
									session.rpcs[UUID].stats._bytesReceived_video = stat.bytesReceived;
								} else if ("kind" in stat && stat.kind == "audio") {
									if ("_bytesReceived_audio" in session.rpcs[UUID].stats) {
										session.rpcs[UUID].stats.audioBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_audio) / ((1024 * session.statsInterval) / 8000));
									}
									session.rpcs[UUID].stats._bytesReceived_audio = stat.bytesReceived;
								}
							}
						}
					} catch (e) {
						errorlog(e);
					}
				});

				//////////

				if (nominatedCandidate) {
					if (nominatedCandidate.localCandidateId && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id !== nominatedCandidate.localCandidateId) {
						if ("candidateType" in nominatedCandidate) {
							try {
								delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_local;
								delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP;
								delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol;
							} catch (e) {}
						}
					}
					if (nominatedCandidate.remoteCandidateId && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id !== nominatedCandidate.remoteCandidateId) {
						if ("candidateType" in nominatedCandidate) {
							try {
								delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_remote;
								delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP;
								delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol;
							} catch (e) {}
						}
					}
					if (nominatedCandidate.localCandidateId) {
						session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id = nominatedCandidate.localCandidateId;
					}
					if (nominatedCandidate.remoteCandidateId) {
						session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id = nominatedCandidate.remoteCandidateId;
					}
					if ("currentRoundTripTime" in nominatedCandidate) {
						session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime * 1000;
					}
				}

				if (nominatedCandidate && nominatedCandidate.localCandidateId) {
					if (candidates[nominatedCandidate.localCandidateId]) {
						var candidate = candidates[nominatedCandidate.localCandidateId];

						if ("candidateType" in candidate) {
							session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_local = candidate.candidateType;

							if (candidate.candidateType === "relay") {
								if ("relayProtocol" in candidate) {
									session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol = candidate.relayProtocol;
								} else {
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol;
								}
								if ("ip" in candidate) {
									session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP = candidate.ip;
								} else {
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP;
								}

								session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_ip_blocking = !ipleakingAllowedLocal;
							} else {
								try {
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP;
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol;
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_ip_blocking;
								} catch (e) {}
							}
						}

						if ("networkType" in candidate) {
							session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_networkType = candidate.networkType;
						}
					}
				}
				if (nominatedCandidate && nominatedCandidate.remoteCandidateId) {
					if (candidates[nominatedCandidate.remoteCandidateId]) {
						var candidate = candidates[nominatedCandidate.remoteCandidateId];
						if ("candidateType" in candidate) {
							session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_remote = candidate.candidateType;

							if (candidate.candidateType === "relay") {
								if ("ip" in candidate) {
									session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP = candidate.ip;
								} else {
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP;
								}

								if ("relayProtocol" in candidate) {
									session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol = candidate.relayProtocol;
								} else {
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol;
								}

								session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_ip_blocking = !ipleakingAllowedRemote;
							} else {
								try {
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP;
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol;
									delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_ip_blocking;
								} catch (e) {}
							}
						}

						if ("networkType" in candidate) {
							session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_networkType = candidate.networkType;
						}
					}
				}

				playoutdelay(UUID);

				setTimeout(function () {
					session.directorSpeakerMute();
					session.directorDisplayMute();
				}, 0);
			});
		}
	} catch (e) {
		errorlog(e);
	}

	pokeIframeAPI("view-stats-updated", true, UUID);
}

function createConnectionDetailsEle(UUID) {
	if (!session.rpcs[UUID]) {
		return false;
	}

	session.rpcs[UUID].connectionDetails = document.createElement("div");
	session.rpcs[UUID].connectionDetails.id = "remoteConnections_" + UUID;
	if (session.rpcs[UUID].stats.info && "total_outbound_p2p_connections" in session.rpcs[UUID].stats.info) {
		session.rpcs[UUID].connectionDetails.innerText = "🔗" + session.rpcs[UUID].stats.info.total_outbound_p2p_connections;
		session.rpcs[UUID].connectionDetails.dataset.value = session.rpcs[UUID].stats.info.total_outbound_p2p_connections;
	}
	session.rpcs[UUID].connectionDetails.dataset.UUID = UUID;
	session.rpcs[UUID].connectionDetails.title = getTranslation("viewer-count");
	session.rpcs[UUID].connectionDetails.className = "rem-con-count";

	session.rpcs[UUID].connectionDetails.addEventListener("click", function (e) {
		// show stats of video if double clicked
		log("clicked connectionDetails icon ");
		try {
			e.preventDefault();
			if (session.statsMenu !== false) {
				var uid = e.currentTarget.dataset.UUID;
				if ("stats" in session.rpcs[uid]) {
					var [menu, innerMenu] = statsMenuCreator();
					printViewStats(innerMenu, uid);
					menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
				}
			}
			e.stopPropagation();
			return false;
		} catch (e) {
			errorlog(e);
		}
	});
	return true;
}

function playoutdelay(UUID) {
	// applies a delay to all videos
	try {
		if ((session.rpcs[UUID].buffer !== false) || (session.buffer!=false) || (session.audioBuffer!==false)) {
			
			// if buffer is set, then session.sync will be set; at least to 0.
			var receivers = getReceivers2(UUID).reverse() || []; //session.rpcs[UUID].getReceivers().reverse();

			if (session.rpcs[UUID].whep) {
				receivers = receivers.concat(getReceiversMC(UUID).reverse()); // if I try to reuse getReceivers2, I get some confused stats (not able to tell tracks apart) TODO: see if this issue is a problem else where, esp with screen shares. sstype==3
			}
			receivers.forEach(function (receiver) {
				try {
					for (var tid in session.rpcs[UUID].stats) {
						if (typeof session.rpcs[UUID].stats[tid] == "object" && "_trackID" in session.rpcs[UUID].stats[tid] && session.rpcs[UUID].stats[tid]._trackID === receiver.track.id && session.rpcs[UUID].stats[tid]._type == receiver.track.kind && "Jitter_Buffer_ms" in session.rpcs[UUID].stats[tid]) {
							//if (ChromiumVersion<=103){ // I don't know the exact version, except I know OBS Studio is 103 and it uses the old way still.netwqor
							var sync_offset = 0.0;

							if (session.rpcs[UUID].stats[tid]._sync_offset) {
								sync_offset = session.rpcs[UUID].stats[tid]._sync_offset || 0;
							} else {
								session.rpcs[UUID].stats[tid]._sync_offset = 0;
							}
							var target_buffer = 0;
							
							if (session.rpcs[UUID].stats[tid]._type == "audio") {
								 if (session.audioBuffer!==false){
									target_buffer = parseFloat(session.audioBuffer || 0);
								 } else {
									target_buffer = parseFloat(session.buffer || 0);
								 }
							} else {
								target_buffer = parseFloat(session.buffer || 0);
							}
							
							if (session.rpcs[UUID].buffer !== false){
								target_buffer = parseFloat(session.rpcs[UUID].buffer);
							}

							sync_offset += target_buffer;
							sync_offset -= session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0;

							if (session.includeRTT && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) {
								sync_offset -= parseInt(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms / 2) || 0; // I can't be sure what the actual one-way delay is
							}

							if (sync_offset > target_buffer) {
								sync_offset = target_buffer || 0;
							}

							if (sync_offset < 0) {
								sync_offset = 0;
							}

							session.rpcs[UUID].stats[tid].Added_Buffer_Delay_ms = sync_offset || 0;

							if (session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) {
								session.rpcs[UUID].stats[tid].Total_Playout_Delay_ms = sync_offset + parseInt(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms / 2) + session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0;
							}

							if (session.rpcs[UUID].stats[tid]._type == "audio") {
								session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0;

								if ("jitterBufferTarget" in receiver){
									receiver.jitterBufferTarget = parseFloat(sync_offset) || 0;
								} else {
									receiver.playoutDelayHint = parseFloat(sync_offset / 1000) || 0;
								}
								// receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe

								if (session.sync !== false) {
									var audio_delay = session.sync || 0; // video is typically showing greater delay than audio.
									audio_delay += target_buffer - session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0;

									if (receiver.track.kind == "audio" && receiver.track.id in session.rpcs[UUID].inboundAudioPipeline) {
										if (session.rpcs[UUID].inboundAudioPipeline[receiver.track.id] && session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode) {
											if (audio_delay < 0) {
												audio_delay = 0;
											}
											try {
												session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.linearRampToValueAtTime(parseFloat(audio_delay / 1000.0), session.audioCtx.currentTime + parseFloat(session.statsInterval / 9000));
											} catch (e) {
												session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.setValueAtTime(parseFloat(audio_delay / 1000.0), session.audioCtx.currentTime + 1);
											}
											session.rpcs[UUID].stats[tid].Audio_Sync_Delay_ms = audio_delay || 0;
										}
									}
								}
							} else if (session.rpcs[UUID].stats[tid]._type == "video") {
								session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0;
								if ("jitterBufferTarget" in receiver){
									receiver.jitterBufferTarget = parseFloat(sync_offset) || 0;
								} else {
									receiver.playoutDelayHint = parseFloat(sync_offset / 1000) || 0;
								}
								// receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe
							}
						}
					}
				} catch (e) {
					errorlog(e);
				}
			});
		}
	} catch (e) {
		errorlog(e);
		warnlog("device does not support playout delay");
	}
}

function printViewStats(menu, UUID) {
	// Stats for viewing a remote video
	if (session.statsMenu === false) {
		return false;
	}

	if (!session.rpcs[UUID]) {
		menu.innerHTML = "<br /><br /><br />Remote Publisher Disconnected";
		return false;
	}

	var statsObj = session.rpcs[UUID].stats;
	var streamID = session.rpcs[UUID].streamID;
	var scrollLeft = menu.scrollLeft;
	var scrollTop = menu.scrollTop;
	menu.innerHTML = "StreamID: <b>" + streamID + "</b><br />";

	//// doesn't work on viewer side.
	//if (session.rpcs && session.rpcs[UUID] && session.rpcs[UUID] && session.rpcs[UUID].restartIce){ // only show if available
	//	menu.innerHTML += "<button onclick='session.rpcs[\""+UUID+"\"].restartIce();'>Restart connection</button>";
	//}

	menu.innerHTML += printValues(statsObj);
	menu.scrollTop = scrollTop;
	menu.scrollLeft = scrollLeft;
	return true;
}

function plotDataSimple(canvas, bitrate, nacks = 0) {
	canvas.height = 50;
	canvas.width = 124;
	canvas.className = "canvasStats";
	var context = canvas.getContext("2d");
	if (isNaN(bitrate)) {
		bitrate = 0;
	}
	if (isNaN(nacks)) {
		nacks = 0;
	}
	var height = context.canvas.height;
	var width = context.canvas.width;

	var val = (10 - nacks) / 10;
	if (val > 1) {
		val = 1;
	} else if (val < 0) {
		val = 0;
	}

	var yScale = height / 4000;
	var x = width - 1;
	var y = height - bitrate * yScale;
	var w = 1;

	context.fillStyle = getColor(val);
	context.fillRect(x, y, w, height);
	context.fillStyle = "#FFFFFF55";
	context.fillRect(x, y - 2, w, 4);

	if (y - 5 > 0) {
		context.fillStyle = "#FFFFFF44";
		context.fillRect(x, y + 2, w, 1);
	}

	context.putImageData(context.getImageData(1, 0, width - 1, height), 0, 0);
	context.clearRect(width - 1, 0, 1, height);
}

function printValues(obj, sort = false) {
	// see: printViewStats
	var out = "";

	var keys = Object.keys(obj);

	if (sort) {
		keys.sort();
	}
	var lat = false;
	var lon = false;

	keys.forEach(key => {
		if (key.startsWith("_")) return; 
		if (typeof obj[key] === "object" && obj[key] !== null) {
			let tmp = sanitizeChat(key);
			out += `<li><h2 title='${tmp}'>${tmp}</h2></li>`;
			if (key == "info") {
				out += printValues(obj[key]);
			} else if (key == "meta") {
                out += `<li class="meta-container">`;
                Object.entries(obj[key]).forEach(([category, data]) => {
                    if (data.type === "file" && data.filetype && data.filetype.startsWith("image/")) {
                        out += `<div class="meta-item meta-image">
                            <img src="${data.value}" alt="${data.label || category}" 
                                style="max-width:50px;max-height:50px;object-fit:contain;">
                            <div class="meta-image-info">
                                <small>${category}</small>
                                <small>${formatFileSize(data.size)}</small>
                            </div>
                        </div>`;
                    } else if (data.type === "url") {
                        out += `<div class="meta-item meta-url">
                            <small>${category}:</small>
                            <a href="${data.value}" target="_blank" rel="noopener" 
                               class="meta-link" title="${data.value}">
                                ${truncateUrl(data.value)}
                            </a>
                        </div>`;
                    } else {
                        out += `<div class="meta-item">
                            <small>${category}:</small>
                            <span class="meta-value">${data.value || ''}</span>
                        </div>`;
                    }
                });
                
                out += `</li>`;
			} else {
				out += printValues(obj[key], true);
			}
		} else {
			try {
				var unit = "";

				var value = obj[key];

				var stat = sanitizeChat(key);

				stat = stat.charAt(0).toUpperCase() + stat.slice(1);

				var hint = "";

				if (typeof obj[key] == "string") {
					value = sanitizeChat(value);
				}

				if (key == "useragent") {
					value = "<span style='cursor: pointer;' onclick='copyFunction(this.innerText,event);' title='Copy this user-agent to the clipboard' style='cursor:pointer'>" + value + "</span>";
				}

				if (key == "Bitrate_in_kbps") {
					var unit = " kbps";
					stat = "Bitrate";
					hint = "You can refer to the documentation for ways to increase the target bitrate";
				} else if (key == "type") {
					var unit = "";
					stat = "Type";

					if (value == "Audio Track") {
						value = "🔊 " + value;
						//out += "<button onclick='disableTrack()'></button>";
					}

					if (value == "Video Track") {
						value = "📺 " + value;
					}
				} else if (key == "packetLoss_in_percentage") {
					var unit = " %";
					stat = "Packet Loss 📶";
					value = parseInt(parseFloat(value) * 10000) / 10000.0;
					hint = "A high packet loss will lower quality of the media";
				} else if (key == "local_relay_IP") {
					value = "<a href='https://whatismyipaddress.com/ip/" + value + "' target='_blank'>" + value + "</a>";
				} else if (key == "remote_relay_IP") {
					value = "<a href='https://whatismyipaddress.com/ip/" + value + "' target='_blank'>" + value + "</a>";
				} else if (key == "local_ip_blocking" && value) {
					console.warn("Your system or connection is blocking p2p traffic");
					value = "⚠️ You're blocking";
					hint = "no direct p2p connection made because of YOUR browser or system setting";
				} else if (key == "remote_ip_blocking" && value) {
					console.warn("A remote client is blocking p2p traffic");
					value = "⚠️ They're blocking";
					hint = "no direct p2p connection made because of THEIR browser or system setting";
				} else if (key == "candidateType_local" && value == "relay") {
					value = "💸 relay server";
					hint = "no direct p2p connection made; using the TURN relay servers.";
					stat = "Candidate type - Local";
				} else if (key == "candidateType_remote" && value == "relay") {
					value = "💸 relay server";
					hint = "no direct p2p connection made; using the TURN relay servers.";
					stat = "Candidate type - Remote";
				} else if (key == "candidateType_local" && value == "host") {
					hint = "No NAT firewall, typical of LAN to LAN";
					stat = "Candidate type - Local";
				} else if (key == "candidateType_remote" && value == "host") {
					hint = "No NAT firewall, typical of LAN to LAN";
					stat = "Candidate type - Remote";
				} else if (key == "candidateType_local" && value == "srflx") {
					hint = "direct p2p, but NAT firewall likely";
					stat = "Candidate type - Local";
				} else if (key == "candidateType_remote" && value == "srflx") {
					hint = "direct p2p, but NAT firewall likely";
					stat = "Candidate type - Remote";
				} else if (key == "height_url") {
					if (value == false) {
						return;
					}
				} else if (key == "width_url") {
					if (value == false) {
						return;
					}
				} else if (key == "height_url") {
					if (value == false) {
						return;
					}
				} else if (key == "version") {
					stat = "VDO.Ninja Version";
				} else if (key == "platform") {
					stat = "Platform (OS)";
				} else if (key == "iPhone12Up") {
					stat = "iPhone 12 and up";
				} else if (key == "aec_url") {
					stat = "Echo-Cancellation";
				} else if (key == "agc_url") {
					stat = "Auto-Gain (agc)";
				} else if (key == "denoise_url") {
					stat = "De-noising ";
				} else if (key == "audio_level") {
					stat = "Audio Level";
				} else if (key == "Jitter_Buffer_ms") {
					var unit = " ms";
					stat = "Jitter Buffer Delay";
				} else if (key == "Added_Buffer_Delay_ms") {
					var unit = " ms";
					stat = "Added Buffer Delay";
					hint = "Value of playout buffer delay added if using &buffer";
				} else if (key == "Total_Playout_Delay_ms") {
					// doesn't include bluetooth / monitor / capture delay, etc.
					var unit = " ms";
					stat = "Total Playout Delay";
					hint = "Network latency + Jitter buffer + any manually added playout delay";
				} else if (value === null) {
					value = "null";
				} else if (key == "stereo_url") {
					stat = "Pro-Audio<br />(Stereo-mode)";
					if (value == 3) {
						value = "3 (outbound hi-fi)<br />Use Headphones";
					} else if (value == 1) {
						value = "1 (in & out hi-fi)<br />Use Headphones";
					} else if (value == 2) {
						value = "3 (inbound hi-fi)";
					} else if (value == 4) {
						value = "3 (multichannel)<br />Use Headphones";
					} else if (value == 5) {
						value = "5 (auto-mode)<br />Use Headphones";
					}
				} else if (value === false) {
					return;
				} else if (value === "false") {
					return;
				} else if (key == "lat") {
					lat = value;
					if (lat && lon) {
						const mapWidth = 250;
						const mapHeight = 250;
						const x = (lon + 180) * (mapWidth / 360);
						const mapRatio = mapHeight / mapWidth;
						const latRad = (lat * Math.PI) / 180;
						const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2));
						const y = mapHeight / 2 - ((mapWidth * mercN) / (2 * Math.PI)) * mapRatio;

						out +=
							'<div class="map-container" style="position: relative;width:' +
							mapWidth +
							"px;height:" +
							mapHeight +
							'px;">\
							<img src="./media/Wikimedia_mercator_modded_cc_by-sa_3.0.png" alt="World Map" style="width:' +
							mapWidth +
							"px; height:" +
							mapHeight +
							';">\
							<div class="marker" style="position: absolute; width: 6px; height: 6px; background-color: red; border-radius: 50%; left: ' +
							(x - 3) +
							"px; top: " +
							(y - 3) +
							'px;"></div>\
						</div>';
					}
				} else if (key == "lon") {
					lon = value;
					if (lat && lon) {
						const mapWidth = 250;
						const mapHeight = 250;
						const x = (lon + 180) * (mapWidth / 360);
						const mapRatio = mapHeight / mapWidth;
						const latRad = (lat * Math.PI) / 180;
						const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2));
						const y = mapHeight / 2 - ((mapWidth * mercN) / (2 * Math.PI)) * mapRatio;

						out +=
							'<div class="map-container" style="position: relative;width:' +
							mapWidth +
							"px;height:" +
							mapHeight +
							'px;">\
							<img src="./media/Wikimedia_mercator_modded_cc_by-sa_3.0.png" alt="World Map" style="width:' +
							mapWidth +
							"px; height:" +
							mapHeight +
							';">\
							<div class="marker" style="position: absolute; width: 6px; height: 6px; background-color: red; border-radius: 50%; left: ' +
							(x - 3) +
							"px; top: " +
							(y - 3) +
							'px;"></div>\
						</div>';
						out += '<a href="https://www.google.com/maps?q=' + lat + "," + lon + '" style="color:lightblue;text-align:center;margin:0 auto;" target="_blank">Open Location in Google Maps</a>';
					}
				} 

				stat = stat.replaceAll("_", " ");
				stat = stat.trim();

				if (hint) {
					out += "<li style='cursor:help;' title='" + hint + "'><span>" + stat + "</span><span>" + value + unit + "</span></li>";
				} else {
					out += "<li><span>" + stat + "</span><span>" + value + unit + "</span></li>";
				}
			} catch (e) {
				warnlog(e);
			}
		
		}
	});
	return out;
}

function processMeshcastStats(UUID) {
	try {
		session.rpcs[UUID].whep.getStats().then(function (stats) {
			if (!(UUID in session.rpcs)) {
				return;
			}

			if (!session.rpcs[UUID].stats["WHEP_Connection"]) {
				// meshcast
				session.rpcs[UUID].stats["WHEP_Connection"] = {};
			}

			// var qos = false;]

			var nominatedCandidate = false;
			var candidates = {};
			var ipleakingAllowedRemote = false;
			var ipleakingAllowedLocal = false;

			stats.forEach(stat => {
				if (stat.id && stat.id.startsWith("DEPRECATED_")) {
					return;
				}

				var trackID = stat.trackIdentifier || stat.id || false;

				if (stat.type == "remote-candidate") {
					candidates[stat.id] = stat;
					if (stat.candidateType != "relay") {
						ipleakingAllowedRemote = true;
					}
				} else if (stat.type == "local-candidate") {
					candidates[stat.id] = stat;
					if (stat.candidateType != "relay") {
						ipleakingAllowedLocal = true;
					}
				} else if (stat.type == "candidate-pair" && stat.nominated) {
					if (!nominatedCandidate) {
						nominatedCandidate = stat;
					} else if (nominatedCandidate.priority < stat.priority) {
						nominatedCandidate = stat;
					}
				} else if (stat.type == "track" && stat.remoteSource) {
					if (stat.id in session.rpcs[UUID].stats) {
						session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier;
						session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0;
						session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
						session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;

						if ("frameWidth" in stat) {
							if ("frameHeight" in stat) {
								session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth + " x " + stat.frameHeight;
								session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth;
								session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight;
							}
						}
					} else {
						session.rpcs[UUID].stats[stat.id] = {};
						session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
						session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
						session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = 0;
						session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier;

						if (stat.kind && stat.kind == "audio") {
							session.rpcs[UUID].stats[stat.id].type = "Audio Track";
							session.rpcs[UUID].stats[stat.id]._type = "audio";
						} else if (stat.kind && stat.kind == "video") {
							session.rpcs[UUID].stats[stat.id].type = "Video Track";
							session.rpcs[UUID].stats[stat.id]._type = "video";
						}
					}
				} else if (stat.type == "transport") {
					if ("bytesReceived" in stat) {
						if ("_bytesReceived" in session.rpcs[UUID].stats["WHEP_Connection"]) {
							if (session.rpcs[UUID].stats["WHEP_Connection"]._timestamp) {
								if (stat.timestamp) {
									session.rpcs[UUID].stats["WHEP_Connection"].total_recv_bitrate_kbps = parseInt((8 * (stat.bytesReceived - session.rpcs[UUID].stats["WHEP_Connection"]._bytesReceived)) / (stat.timestamp - session.rpcs[UUID].stats["WHEP_Connection"]._timestamp));
								}
							}
						}
						session.rpcs[UUID].stats["WHEP_Connection"]._bytesReceived = stat.bytesReceived;
					}
					if ("timestamp" in stat) {
						session.rpcs[UUID].stats["WHEP_Connection"]._timestamp = stat.timestamp;
						if (!session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart) {
							session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart = stat.timestamp;
						} else {
							session.rpcs[UUID].stats["WHEP_Connection"].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart) / 600) / 100;
						}
					}
				} else if (stat.type == "inbound-rtp" && trackID) {
					session.rpcs[UUID].stats[trackID] = session.rpcs[UUID].stats[trackID] || {};

					if (stat.trackIdentifier) {
						session.rpcs[UUID].stats[trackID]._trackID = stat.trackIdentifier;
					}
					if ("jitterBufferDelay" in stat) {
						session.rpcs[UUID].stats[trackID].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[trackID]._jitter_delay_2)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[trackID]._jitter_count_2)) || 0;
						session.rpcs[UUID].stats[trackID]._jitter_delay_2 = parseFloat(stat.jitterBufferDelay) || 0;
						session.rpcs[UUID].stats[trackID]._jitter_count_2 = parseInt(stat.jitterBufferEmittedCount) || 0;
					}

					if ("frameWidth" in stat) {
						if ("frameHeight" in stat) {
							session.rpcs[UUID].stats[trackID].Resolution = stat.frameWidth + " x " + stat.frameHeight;
							session.rpcs[UUID].stats[trackID]._frameWidth = stat.frameWidth;
							session.rpcs[UUID].stats[trackID]._frameHeight = stat.frameHeight;
						}
					}

					session.rpcs[UUID].stats[trackID].Bitrate_in_kbps = parseInt((8 * (stat.bytesReceived - (session.rpcs[UUID].stats[trackID]._last_bytes || 0))) / (stat.timestamp - session.rpcs[UUID].stats[trackID]._last_time));

					session.rpcs[UUID].stats[trackID]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[trackID]._last_bytes;
					session.rpcs[UUID].stats[trackID]._last_time = stat.timestamp || session.rpcs[UUID].stats[trackID]._last_time;

					session.rpcs[UUID].stats._codecId = stat.codecId;
					session.rpcs[UUID].stats._codecIdTrackId = trackID;

					if (stat.mediaType == "video") {
						session.rpcs[UUID].stats[trackID].type = "Video Stream";
						session.rpcs[UUID].stats[trackID]._type = "video";
						if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP8") {
							session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0;
							session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0;

							log("OBS PLI FIX MODE ON");
							if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix) {
								// heavy packet loss with no pliCount?
								session.requestKeyframe(UUID);
								session.rpcs[UUID].stats[trackID].nackTrigger = 0;
								log("TRYING KEYFRAME");
							} else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) {
								session.rpcs[UUID].stats[trackID].nackTrigger = 0;
							}
						} else if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP9") {
							session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0;
							session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0;

							log("OBS PLI FIX MODE ON");
							if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix * 4) {
								// heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often.
								session.requestKeyframe(UUID);
								session.rpcs[UUID].stats[trackID].nackTrigger = 0;
								log("TRYING KEYFRAME");
							} else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) {
								session.rpcs[UUID].stats[trackID].nackTrigger = 0;
							}
						}

						session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0;
						session.rpcs[UUID].stats[trackID].streamErrors_nackCount = stat.nackCount || 0;

						//warnlog(stat);

						if ("framesPerSecond" in stat) {
							session.rpcs[UUID].stats[trackID].FPS = parseInt(stat.framesPerSecond);
						} else if ("framesDecoded" in stat && stat.timestamp) {
							var lastFramesDecoded = 0;
							var lastTimestamp = 0;
							try {
								lastFramesDecoded = session.rpcs[UUID].stats[trackID]._framesDecoded;
								lastTimestamp = session.rpcs[UUID].stats[trackID]._timestamp;
							} catch (e) {}
							session.rpcs[UUID].stats[trackID].FPS = parseInt((10 * (stat.framesDecoded - lastFramesDecoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10;

							//session.rpcs[UUID].stats[trackID].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp));
							session.rpcs[UUID].stats[trackID]._framesDecoded = stat.framesDecoded;
							session.rpcs[UUID].stats[trackID]._timestamp = stat.timestamp / 1000;
						}
					} else if (stat.mediaType == "audio") {
						//log("AUDIO LEVEL: "+stat.audioLevel);
						session.rpcs[UUID].stats[trackID].type = "Audio Stream";
						session.rpcs[UUID].stats[trackID]._type = "audio";
						if ("audioLevel" in stat) {
							session.rpcs[UUID].stats[trackID].audio_level = parseInt(parseFloat(stat.audioLevel) * 10000) / 10000.0;
						}
					}

					if ("packetsLost" in stat && "packetsReceived" in stat) {
						if (!("_packetsLost" in session.rpcs[UUID].stats[trackID])) {
							session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost;
						}
						if (!("_packetsReceived" in session.rpcs[UUID].stats[trackID])) {
							session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived;
						}

						if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[trackID])) {
							session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = 0;
						}

						session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage * 0.35 + (0.65 * ((stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost) * 100.0)) / (stat.packetsReceived - session.rpcs[UUID].stats[trackID]._packetsReceived + (stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost)) || 0;

						if (session.rpcs[UUID].stats[trackID]._type === "video") {
							qos = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage; // packet loss of video track
						}

						if (session.rpcs[UUID].signalMeter && session.rpcs[UUID].stats[trackID]._type === "video") {
							if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.01) {
								if (session.rpcs[UUID].stats[trackID].Bitrate_in_kbps == 0) {
									session.rpcs[UUID].signalMeter.dataset.level = 0;
								} else {
									session.rpcs[UUID].signalMeter.dataset.level = 5;
								}
							} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.3) {
								session.rpcs[UUID].signalMeter.dataset.level = 4;
							} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 1.0) {
								session.rpcs[UUID].signalMeter.dataset.level = 3;
							} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 3.5) {
								session.rpcs[UUID].signalMeter.dataset.level = 2;
							} else {
								session.rpcs[UUID].signalMeter.dataset.level = 1;
							}
						}

						session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived;
						session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost;
					}
				} else if ("_codecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._codecId) {
					if ("mimeType" in stat) {
						if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]) {
							session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
						} else {
							session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {};
							session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
						}
					}
					if ("frameHeight" in stat) {
						if ("frameWidth" in stat) {
							session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth) + " x " + parseInt(stat.frameHeight);
						}
					}
				} else if (Firefox) {
					if ("mimeType" in stat && "type" in stat && "id" in stat && stat.type == "codec") {
						if (stat.mimeType.includes("video")) {
							session.rpcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1];
						} else if (stat.mimeType.includes("audio")) {
							session.rpcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1];
							if (stat.clockRate) {
								session.rpcs[UUID].stats.audio_clockRate = stat.clockRate;
								if (stat.channels) {
									session.rpcs[UUID].stats.audio_clockRate += " / " + stat.channels;
								}
							} else if (stat.sdpFmtpLine) {
								session.rpcs[UUID].stats.fmtp = stat.sdpFmtpLine;
							}
						}
					}

					if ("frameWidth" in stat) {
						session.rpcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight;
						if ("framesPerSecond" in stat) {
							session.rpcs[UUID].stats.resolution += " @ " + stat.framesPerSecond;
						}
					}

					if ("bytesReceived" in stat) {
						if ("kind" in stat && stat.kind == "video") {
							if ("_bytesReceived_video" in session.rpcs[UUID].stats) {
								session.rpcs[UUID].stats.videoBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_video) / ((1024 * session.statsInterval) / 8000));
							}
							session.rpcs[UUID].stats._bytesReceived_video = stat.bytesReceived;
						} else if ("kind" in stat && stat.kind == "audio") {
							if ("_bytesReceived_audio" in session.rpcs[UUID].stats) {
								session.rpcs[UUID].stats.audioBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_audio) / ((1024 * session.statsInterval) / 8000));
							}
							session.rpcs[UUID].stats._bytesReceived_audio = stat.bytesReceived;
						}
					}
				}
			});

			////////////

			if (nominatedCandidate) {
				if ("currentRoundTripTime" in nominatedCandidate) {
					session.rpcs[UUID].stats["WHEP_Connection"].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime * 1000;
				}
			}

			if (nominatedCandidate && nominatedCandidate.remoteCandidateId) {
				if (candidates[nominatedCandidate.remoteCandidateId]) {
					var candidate = candidates[nominatedCandidate.remoteCandidateId];
					if ("candidateType" in candidate) {
						session.rpcs[UUID].stats["WHEP_Connection"].candidateType_remote = candidate.candidateType;
						if (candidate.candidateType === "relay") {
							if ("relayProtocol" in candidate) {
								session.rpcs[UUID].stats["WHEP_Connection"].remote_relay_protocol = candidate.relayProtocol;
							}
							if ("ip" in candidate) {
								session.rpcs[UUID].stats["WHEP_Connection"].remote_relay_IP = candidate.ip;
							}
						} else {
							try {
								delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP;
								delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol;
							} catch (e) {}
						}
						if ("networkType" in candidate) {
							session.rpcs[UUID].stats["WHEP_Connection"].remote_networkType = candidate.networkType;
						}
					}
				}
			}
			if (nominatedCandidate && nominatedCandidate.localCandidateId) {
				if (candidates[nominatedCandidate.localCandidateId]) {
					var candidate = candidates[nominatedCandidate.localCandidateId];
					if ("candidateType" in candidate) {
						session.rpcs[UUID].stats["WHEP_Connection"].candidateType_local = candidate.candidateType;
						if (candidate.candidateType === "relay") {
							if ("relayProtocol" in candidate) {
								session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol = candidate.relayProtocol;
							}
							if ("ip" in candidate) {
								session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP = candidate.ip;
							}
						} else {
							try {
								delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP;
								delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol;
							} catch (e) {}
						}
					}

					if ("networkType" in candidate) {
						session.rpcs[UUID].stats["WHEP_Connection"].local_networkType = candidate.networkType;
					}
				}
			}

			/* try{ // we want to let meshcast know if our node is getting overloaded, to avoid making it worse
				if ((qos!==false) &&  session.rpcs[UUID].settings && session.rpcs[UUID].settings.url){
					var request = new XMLHttpRequest();
					var node = session.rpcs[UUID].settings.url.split("https://")[1].split(".meshcast.io")[0];
					if (node){
						request.open('POST', " https://qos.meshcast.io/?name="+node);
						request.send(qos);
					}
				}
			} catch(e){
				errorlog(e);
			} */

			//if (session.buffer!==false){
			playoutdelay(UUID); // it will handle itself for now on I guess
			//}
		});
	} catch (e) {
		errorlog(e);
	}
}

function safeAppendToMenu(menu, text) {
	const li = document.createElement("li");
	const h2 = document.createElement("h2");
	h2.title = text;
	h2.textContent = text; // Safely assigns text content, avoiding HTML parsing
	li.appendChild(h2);
	menu.appendChild(li);
}

function printMyStats(menu, screenshare = false) {
	// see: setupStatsMenu

	if (!session) {
		return;
	}

	var scrollLeft = getById("menuStatsBox").scrollLeft;
	var scrollTop = getById("menuStatsBox").scrollTop;
	menu.innerHTML = "";

	try {
		session.stats.outbound_connections = Object.keys(session.pcs).length;
		session.stats.inbound_connections = Object.keys(session.rpcs).length;
	} catch (e) {}

	try {
		var obscam = false;
		if (document.querySelector("select#videoSource3")) {
			var videoSelect = document.querySelector("select#videoSource3").options;
			if (videoSelect.length) {
				if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) {
					// OBS Virtualcam
					obscam = true;
				} else if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) {
					// OBS Virtualcam
					obscam = true;
				}
			}
		}

		if (session.streamSrc) {
			session.streamSrc.getVideoTracks().forEach(function (track) {
				session.currentCameraConstraints = track.getSettings();

				if (screen && screen.orientation && screen.orientation.type) {
					if (!screen.orientation.type.includes("portrait")) {
						if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
							session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
						}
					}
				} else if (!window.matchMedia("(orientation: portrait)").matches) {
					// legacy
					if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
						session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
					}
				}

				if (obscam && parseInt(session.currentCameraConstraints.frameRate) == 30) {
					session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0);
				} else {
					var frameRateFPS = session.currentCameraConstraints.frameRate;
					if (frameRateFPS) {
						session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0) + " @ " + parseInt(frameRateFPS * 100) / 100.0 + "fps";
					} else {
						session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0);
					}
				}
			});
		}
	} catch (e) {
		errorlog(e);
	}

	function printViewValues(obj, UUID = false) {
		if (!document.getElementById("menuStatsBox")) {
			return;
		}

		var keys = Object.keys(obj).sort();

		keys.forEach(key => {
			if (typeof obj[key] === "object") {
				try {
					let sanitizedKey = sanitizeChat(key);
					if (sanitizedKey === "info") {
						sanitizedKey = "Remote Peer Info";
					}
					safeAppendToMenu(menu, sanitizedKey);
				} catch (e) {
					errorlog(e);
				}

				try {
					printViewValues(obj[key]);
				} catch (e) {
					errorlog(e);
				}

				menu.innerHTML += "<hr />";
			}
		});

		if (session.promptAccess) {
			if (UUID && session.pcs[UUID]) {
				menu.innerHTML += "<button onclick='session.closePC(\"" + UUID + "\");' style='border:solid 3px red;' title='This will disconnect this viewer from your stream.'>Disconnect this viewer</button>";
			}
		}

		if (UUID && session.pcs[UUID] && session.pcs[UUID].restartIce) {
			// only show if available
			menu.innerHTML += "<button onclick='session.pcs[\"" + UUID + "\"].restartIce();' title='This will trigger an ICE Restart, which may or may not help with some connection issues'>Trigger an ICE restart</button>";
		}

		keys.forEach(key => {
			if (typeof obj[key] !== "object") {
				if (key.startsWith("_")) {
					return;
				}

				let unit = "";
				let hint = "";

				var stat = sanitizeChat(key);

				stat = stat.charAt(0).toUpperCase() + stat.slice(1);

				var value = obj[key];
				if (typeof value == "string") {
					value = sanitizeChat(value);
				}

				if (value === false) {
					return;
				}

				if (key == "useragent") {
					value = "<span style='cursor: pointer;' onclick='copyFunction(this.innerText,event);' title='Copy this user-agent to the clipboard' style='cursor:pointer'>" + value + "</span>";
				}

				if (key == "local_relay_IP") {
					value = "<a href='https://whatismyipaddress.com/ip/" + value + "' target='_blank'>" + value + "</a>";
				}
				if (key == "remote_relay_IP") {
					value = "<a href='https://whatismyipaddress.com/ip/" + value + "' target='_blank'>" + value + "</a>";
				}
				if (key == "watch_URL") {
					value = "<a title='The standalone Meshcast.io view link for this stream' href='" + value + "' target='_blank'>" + value + "</a>";
				}
				if (key == "candidateType_local" && value == "relay") {
					stat = "Candidate type - Local";
					value = "💸 <p style='cursor:help;' title='no direct p2p connection made; using the TURN relay servers.'>relay server</p>";
				} else if (key == "candidateType_remote" && value == "relay") {
					stat = "Candidate type - Remote";
					value = "💸 <p style='cursor:help;' title='no direct p2p connection made; using the TURN relay servers.'>relay server</p>";
				} else if (key == "candidateType_local" && value == "host") {
					stat = "Candidate type - Local";
					value = "<p style='cursor:help;' title='No NAT firewall, typical of LAN to LAN'>host</p>";
				} else if (key == "candidateType_remote" && value == "host") {
					stat = "Candidate type - Remote";
					value = "<p style='cursor:help;' title='No NAT firewall, typical of LAN to LAN'>host</p>";
				} else if (key == "candidateType_local" && value == "srflx") {
					stat = "Candidate type - Local";
					value = "<p style='cursor:help;' title='direct p2p, but NAT firewall likely'>srflx</p>";
				} else if (key == "candidateType_remote" && value == "srflx") {
					stat = "Candidate type - Remote";
					value = "<p style='cursor:help;' title='direct p2p, but NAT firewall likely'>srflx</p>";
				} else if (key == "local_ip_blocking" && value) {
					value = "⚠️ You're blocking";
					hint = "no direct p2p connection made because of YOUR browser or system setting";
				} else if (key == "remote_ip_blocking" && value) {
					value = "⚠️ They're blocking";
					hint = "no direct p2p connection made because of THEIR browser or system setting";
				} else if (key == "label" && value) {
					value = "<b style='color:pink;'>" + value + "</b>";
				}

				stat = stat.replaceAll("_", " ");

				if (hint) {
					menu.innerHTML += "<li style='cursor:help;' title='" + hint + "'><span>" + stat + "</span><span>" + value + unit + "</span></li>";
				} else {
					menu.innerHTML += "<li><span>" + stat + "</span><span>" + value + unit + "</span></li>";
				}
			}
		});

		if (UUID && session.pcs[UUID]) {
			if (session.pcs[UUID].maxBandwidth) {
				menu.innerHTML += "<li><span>max bandwidth target</span><span>" + session.pcs[UUID].maxBandwidth + "</span></li>";
			}
			if (session.pcs[UUID].setBitrate) {
				menu.innerHTML += '<li><span onclick=\'var bitr=prompt("Enter a target bitrate (kbps)");if (bitr!==null){session.limitBitrate("' + UUID + "\", parseInt(bitr));}'>init bitrate target</span><span>" + session.pcs[UUID].setBitrate + "</span></li>";
			}
			if (session.pcs[UUID].savedBitrate) {
				menu.innerHTML += "<li><span>current bitrate target</span><span>" + session.pcs[UUID].savedBitrate + "</span></li>";
			}
			if (session.showSlider || (!session.roomid && !session.pcs[UUID].whipout && session.meshcast !== "audio")) {
				menu.innerHTML += "<li><span title='Only available if not in a group room'>adjust video bitrate</span><span><input class='thinSlider' title='Adjust the outbound bitrate for this stream.' type='range' value='" + (session.pcs[UUID].savedBitrate || session.pcs[UUID].setBitrate || 2500) + "' min='0' max='" + (session.pcs[UUID].setBitrate || 6000) + "' onchange='session.limitBitrate(\"" + UUID + "\", parseInt(this.value));' /></span></li>";

				if (!session.hidehome) {
					menu.innerHTML += "<center><a target='_blank' href='https://docs.vdo.ninja/guides/how-do-i-control-bitrate-quality'>More info on setting bitrates higher here</a></center>";
				}
			}
		}
	}

	printViewValues(session.stats);
	menu.innerHTML += "<button onclick='session.forcePLI(null,event);' data-translate='send-keyframe-to-viewer'>Send Keyframe to Viewers</button>";

	if (!screenshare && session.meshcast && session.whipOut && session.whipOut.stats) {
		printViewValues({ Meshcast_connection: session.whipOut.stats });
		menu.innerHTML += "<hr>";
	} else if (!screenshare && session.whipOut && session.whipOut.stats) {
		printViewValues({ Whip_Out_connection: session.whipOut.stats });
		menu.innerHTML += "<hr>";
	}
	if (!screenshare && session.whepIn && session.whepIn.stats) {
		printViewValues({ Whep_In_connection: session.whepIn.stats });
		menu.innerHTML += "<hr>";
	}

	for (var uuid in session.pcs) {
		if (screenshare) {
			if (session.pcs[uuid].realUUID) {
				printViewValues(session.pcs[uuid].stats, uuid);
				menu.innerHTML += "<hr>";
			}
		} else if (!session.pcs[uuid].realUUID) {
			printViewValues(session.pcs[uuid].stats, uuid);
			menu.innerHTML += "<hr>";
		}
	}
	if (iOS || iPad) {
		menu.innerHTML += "<br /><div style='height:100px'></div>";
	}
	try {
		getById("menuStatsBox").scrollLeft = scrollLeft;
		getById("menuStatsBox").scrollTop = scrollTop;
	} catch (e) {}
}

function updateLocalStats() {
	if (!session) {
		return;
	}

	var totalBitrate = 0;
	var totalBitrate2 = 0;
	var cpuLimited = false;
	var relayUsed = false;
	var totalVideo = 0;
	var totalAudio = 0;
	var totalScenes = 0;
	var meshcastActive = false;
	var nackRate = 0;
	var totalStreams = 0;

	var miscSenders = [];

	if (session.whipOut && session.whipOut.getSenders && session.whipOut.stats) {
		miscSenders.push(session.whipOut);
	}

	miscSenders.forEach(data => {
		try {
			var atot = 0;
			var senders = data.getSenders(); // for any connected peer, update the video they have if connected with a video already.
			senders.forEach(sender => {
				// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
				if (sender.track && sender.track.kind == "video" && sender.track.enabled) {
					meshcastActive = true;
				} else if (sender.track && sender.track.kind == "audio" && sender.track.enabled && !session.muted) {
					meshcastActive = true;
				}
			});
			//totalAudio += atot;

			if ("video_bitrate_kbps" in data.stats) {
				totalBitrate += data.stats.video_bitrate_kbps || 0;
			}
			if ("audio_bitrate_kbps" in data.stats) {
				totalBitrate += data.stats.audio_bitrate_kbps || 0;
			}
			if ("total_sending_bitrate_kbps" in data.stats) {
				totalBitrate2 += data.stats.total_sending_bitrate_kbps || 0;
			}

			if ("quality_limitation_reason" in data.stats) {
				if (data.stats.quality_limitation_reason == "cpu") {
					cpuLimited = true;
				}
			}

			if ("nacks_per_second" in data.stats) {
				nackRate += data.stats.nacks_per_second;
				totalStreams += 1;
			}

			setTimeout(
				function (data) {
					if (!data) {
						return;
					}
					data.getStats().then(function (stats) {
						if ("audio_bitrate_kbps" in data.stats) {
							data.stats.audio_bitrate_kbps = 0;
						}

						var nominatedCandidate = false;
						var candidates = {};
						var ipleakingAllowedRemote = false;
						var ipleakingAllowedLocal = false;

						stats.forEach(stat => {
							if (stat.id && stat.id.startsWith("DEPRECATED_")) {
								return;
							}

							if (stat.type == "transport") {
								if ("bytesSent" in stat) {
									if ("_bytesSent" in data.stats) {
										if (data.stats._timestamp) {
											if (stat.timestamp) {
												data.stats.total_sending_bitrate_kbps = parseInt((8 * (stat.bytesSent - data.stats._bytesSent)) / (stat.timestamp - data.stats._timestamp));
											}
										}
									}
									data.stats._bytesSent = stat.bytesSent;
								}
								if ("timestamp" in stat) {
									data.stats._timestamp = stat.timestamp;
								}
							} else if (stat.type == "outbound-rtp") {
								if (stat.kind == "video") {
									if ("framesPerSecond" in stat) {
										data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond;
									} else if ("frameHeight" in stat) {
										if ("framesEncoded" in stat && stat.timestamp) {
											var lastFramesEncoded = 0;
											var lastTimestamp = 0;
											try {
												lastFramesEncoded = data.stats._framesEncoded;
												lastTimestamp = data.stats._timestamp;
											} catch (e) {}
											data.stats._FPS = parseInt((10 * (stat.framesEncoded - lastFramesEncoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10 || "?";
											data.stats._framesEncoded = stat.framesEncoded;
											data.stats._timestamp = stat.timestamp / 1000;
											data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + data.stats._FPS;
										} else {
											data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight;
										}
									}
									if ("encoderImplementation" in stat) {
										data.stats.video_encoder = stat.encoderImplementation;
										if (stat.encoderImplementation == "ExternalEncoder") {
											data.stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly
											data.encoder = true;
										} else if (stat.encoderImplementation == "MediaFoundationVideoEncodeAccelerator") {
											data.stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly
											data.encoder = true;
										} else {
											data.encoder = false; // this may not be actually accurate, but lets assume so.
										}
									}
									if ("qualityLimitationReason" in stat) {
										if (data.stats.quality_limitation_reason) {
											if (data.stats.quality_limitation_reason !== stat.qualityLimitationReason) {
												try {
													var miniInfo = {};
													miniInfo.qlr = stat.qualityLimitationReason;
													if ("_hardwareEncoder" in data.stats) {
														miniInfo.hw_enc = data.stats._hardwareEncoder;
													} else {
														miniInfo.hw_enc = null;
													}
													session.sendMessage({ miniInfo: miniInfo });
												} catch (e) {
													warnlog(e);
												}
											}
										}
										data.stats.quality_limitation_reason = stat.qualityLimitationReason;
									}

									if ("bytesSent" in stat) {
										if ("_bytesSentVideo" in data.stats) {
											if (data.stats._timestamp1) {
												data.stats.video_bitrate_kbps = parseInt((8 * (stat.bytesSent - data.stats._bytesSentVideo)) / (stat.timestamp - data.stats._timestamp1));
												if (stat.timestamp) {
												}
											}
										}
										data.stats._bytesSentVideo = stat.bytesSent;
									}

									if ("nackCount" in stat) {
										if ("_nackCount" in data.stats) {
											if (data.stats._timestamp1) {
												if (stat.timestamp) {
													data.stats.nacks_per_second = parseInt((10000 * (stat.nackCount - data.stats._nackCount)) / (stat.timestamp - data.stats._timestamp1)) / 10;
												}
											}
										}
									}
									if ("retransmittedBytesSent" in stat) {
										if ("_retransmittedBytesSent" in data.stats) {
											if (data.stats._timestamp1) {
												if (stat.timestamp) {
													data.stats.retransmitted_kbps = parseInt((8 * (stat.retransmittedBytesSent - data.stats._retransmittedBytesSent)) / (stat.timestamp - data.stats._timestamp1));
												}
											}
										}
									}

									if ("nackCount" in stat) {
										data.stats._nackCount = stat.nackCount;
									}

									if ("retransmittedBytesSent" in stat) {
										data.stats._retransmittedBytesSent = stat.retransmittedBytesSent;
									}

									if ("timestamp" in stat) {
										data.stats._timestamp1 = stat.timestamp;
									}

									if ("pliCount" in stat) {
										data.stats.total_pli_count = stat.pliCount;
									}
									if ("keyFramesEncoded" in stat) {
										data.stats.total_key_frames_encoded = stat.keyFramesEncoded;
									}
								} else if (stat.kind == "audio") {
									if ("bytesSent" in stat) {
										if (data.stats._bytesSentAudio) {
											if (data.stats._timestamp2) {
												if (stat.timestamp) {
													if ("audio_bitrate_kbps" in data.stats) {
														data.stats.audio_bitrate_kbps += parseInt((8 * (stat.bytesSent - data.stats._bytesSentAudio)) / (stat.timestamp - data.stats._timestamp2));
													} else {
														data.stats.audio_bitrate_kbps = 0;
													}
												}
											}
										}
									}
									if ("timestamp" in stat) {
										data.stats._timestamp2 = stat.timestamp;
									}

									if ("bytesSent" in stat) {
										data.stats._bytesSentAudio = stat.bytesSent;
									}
								}
							} else if (stat.type == "remote-candidate") {
								candidates[stat.id] = stat;
								if (stat.candidateType != "relay") {
									ipleakingAllowedRemote = true;
								}
							} else if (stat.type == "local-candidate") {
								candidates[stat.id] = stat;
								if (stat.candidateType != "relay") {
									ipleakingAllowedLocal = true;
								}
							} else if (stat.type == "candidate-pair" && stat.nominated) {
								if (!nominatedCandidate) {
									nominatedCandidate = stat;
								} else if (nominatedCandidate.priority < stat.priority) {
									nominatedCandidate = stat;
								}
							} else if ("mimeType" in stat && "type" in stat && stat.type == "codec") {
								if (stat.mimeType.includes("video")) {
									data.stats.video_codec = stat.mimeType.split("video/")[1];
								} else if (stat.mimeType.includes("audio")) {
									data.stats.audio_codec = stat.mimeType.split("audio/")[1];
									if (stat.clockRate) {
										data.stats.audio_clockRate = stat.clockRate;
										if (stat.channels) {
											data.stats.audio_clockRate += " / " + stat.channels;
										}
									} else if (stat.sdpFmtpLine) {
										data.stats.fmtp = stat.sdpFmtpLine;
									}
								}
							}
							return;
						});

						if (nominatedCandidate) {
							if ("availableOutgoingBitrate" in nominatedCandidate) {
								data.stats.available_outgoing_bitrate_kbps = parseInt(nominatedCandidate.availableOutgoingBitrate / 1024);
								if (session.maxBandwidth !== false) {
									session.limitMaxBandwidth(data.stats.available_outgoing_bitrate_kbps, session.pcs[UUID], false);
								}
							}
							if ("totalRoundTripTime" in nominatedCandidate) {
								if ("responsesReceived" in nominatedCandidate) {
									data.stats.average_roundTripTime_ms = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000);
								}
							}
						}
						if (nominatedCandidate && nominatedCandidate.remoteCandidateId) {
							if (candidates[nominatedCandidate.remoteCandidateId]) {
								var candidate = candidates[nominatedCandidate.remoteCandidateId];
								if ("candidateType" in candidate) {
									data.stats.candidateType_remote = candidate.candidateType;
									if (candidate.candidateType === "relay") {
										if ("ip" in candidate) {
											data.stats.remote_relay_IP = candidate.ip;
										}
										if ("relayProtocol" in candidate) {
											data.stats.remote_relay_protocol = candidate.relayProtocol;
										}

										data.stats.remote_ip_blocking = !ipleakingAllowedRemote;
									} else {
										try {
											delete data.stats.remote_relay_IP;
											delete data.stats.remote_relay_protocol;
											delete data.stats.remote_ip_blocking;
										} catch (e) {}
									}
								}
							}
						}
						if (nominatedCandidate && nominatedCandidate.localCandidateId) {
							if (candidates[nominatedCandidate.localCandidateId]) {
								var candidate = candidates[nominatedCandidate.localCandidateId];
								if ("candidateType" in candidate) {
									data.stats.candidateType_local = candidate.candidateType;

									if (candidate.candidateType === "relay") {
										if ("ip" in candidate) {
											data.stats.local_relay_IP = candidate.ip;
										}
										if ("relayProtocol" in candidate) {
											data.stats.local_relay_protocol = candidate.relayProtocol;
										}

										data.stats.local_ip_blocking = !ipleakingAllowedLocal;
									} else {
										try {
											delete data.stats.local_relay_IP;
											delete data.stats.local_relay_protocol;
											delete data.stats.local_ip_blocking;
										} catch (e) {}
									}
								}
							}
						}

						return;
					});
				},
				0,
				data
			);
		} catch (e) {
			errorlog(e);
		}
	});

	for (var uuid in session.pcs) {
		if (!session.pcs[uuid].stats) {
			continue;
		}

		var atot = 0;
		var senders = getSenders2(uuid); // for any connected peer, update the video they have if connected with a video already.
		senders.forEach(sender => {
			// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
			if (sender.track && sender.track.kind == "video" && sender.track.enabled) {
				totalVideo += 1;
			} else if (sender.track && sender.track.kind == "audio" && sender.track.enabled && !session.muted) {
				atot = 1;
			}
		});
		totalAudio += atot;

		if ("scene" in session.pcs[uuid]) {
			if (session.pcs[uuid].scene !== false) {
				totalScenes += 1;
			}
		}

		if ("video_bitrate_kbps" in session.pcs[uuid].stats) {
			totalBitrate += session.pcs[uuid].stats.video_bitrate_kbps || 0;
		}
		if ("audio_bitrate_kbps" in session.pcs[uuid].stats) {
			totalBitrate += session.pcs[uuid].stats.audio_bitrate_kbps || 0;
		}
		if ("total_sending_bitrate_kbps" in session.pcs[uuid].stats) {
			totalBitrate2 += session.pcs[uuid].stats.total_sending_bitrate_kbps || 0;
		}

		if ("quality_limitation_reason" in session.pcs[uuid].stats) {
			if (session.pcs[uuid].stats.quality_limitation_reason == "cpu") {
				cpuLimited = true;
			}
		}

		if ("nacks_per_second" in session.pcs[uuid].stats) {
			nackRate += session.pcs[uuid].stats.nacks_per_second;
			totalStreams += 1;
		}

		if (uuid in session.rpcs) {
			if (session.pcs[uuid].stats.label) {
				session.pcs[uuid].stats.label = session.rpcs[uuid].label;
			}
			if (session.pcs[uuid].stats.streamID) {
				session.pcs[uuid].stats.streamID = session.rpcs[uuid].streamID;
			}
		}

		var screenTracksIds = [];
		if (session.screenStream !== false) {
			// null if already used. false if never used.
			session.screenStream.getTracks().forEach(trk => {
				screenTracksIds.push(trk.id);
			});
		}

		setTimeout(
			function (UUID) {
				if (!session.pcs[UUID]) {
					return;
				}

				if (session.pcs[UUID].realUUID && session.pcs[session.pcs[UUID].realUUID]) {
					var thisIsAlt = true;
					var node = session.pcs[session.pcs[UUID].realUUID];
				} else {
					var thisIsAlt = false;
					var node = session.pcs[UUID];
				}

				node.getStats().then(function (stats) {
					if (!(UUID in session.pcs)) {
						return;
					}

					if ("audio_bitrate_kbps" in session.pcs[UUID].stats) {
						session.pcs[UUID].stats.audio_bitrate_kbps = 0;
					}

					var nominatedCandidate = false;
					var candidates = {};
					var ipleakingAllowedRemote = false;
					var ipleakingAllowedLocal = false;

					var statObject = [];
					var altStreamList = {};
					stats.forEach(stat => {
						statObject.push(stat);
						if (screenTracksIds.includes(stat.trackIdentifier)) {
							altStreamList[stat.id] = stat.trackIdentifier;
						}
					});

					statObject.forEach(stat => {
						if (stat.id && stat.id.startsWith("DEPRECATED_")) {
							return;
						}

						if (stat.type == "transport") {
							if ("bytesSent" in stat) {
								if ("_bytesSent" in session.pcs[UUID].stats) {
									if (session.pcs[UUID].stats._timestamp3) {
										if (stat.timestamp) {
											session.pcs[UUID].stats.total_sending_bitrate_kbps = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSent)) / (stat.timestamp - session.pcs[UUID].stats._timestamp3));
										}
									}
								}
								session.pcs[UUID].stats._bytesSent = stat.bytesSent;
							}
							if ("timestamp" in stat) {
								session.pcs[UUID].stats._timestamp3 = stat.timestamp;
							}
						} else if (stat.type == "outbound-rtp") {
							if (thisIsAlt && stat.mediaSourceId && !altStreamList[stat.mediaSourceId]) {
								// this isn't an alt stream, but we are in alt mode
								return;
							} else if (!thisIsAlt && stat.mediaSourceId && altStreamList[stat.mediaSourceId]) {
								// this is an alt stream, but we are not in alt mode
								return;
							}

							if (stat.kind == "video") {
								if ("framesPerSecond" in stat) {
									session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond;
								} else if ("frameHeight" in stat) {
									if ("framesEncoded" in stat && stat.timestamp) {
										var lastFramesEncoded = 0;
										var lastTimestamp = 0;
										try {
											lastFramesEncoded = session.pcs[UUID].stats._framesEncoded;
											lastTimestamp = session.pcs[UUID].stats._timestamp;
										} catch (e) {}
										session.pcs[UUID].stats._FPS = parseInt((10 * (stat.framesEncoded - lastFramesEncoded)) / (stat.timestamp - lastTimestamp)) / 10;
										session.pcs[UUID].stats._framesEncoded = stat.framesEncoded;
										session.pcs[UUID].stats._timestamp = stat.timestamp;
										session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + session.pcs[UUID].stats._FPS;
									} else {
										session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight;
									}
								}

								var miniInfo = {};
								var sendMini = false;

								if ("encoderImplementation" in stat) {
									session.pcs[UUID].stats.video_encoder = stat.encoderImplementation;

									if (stat.encoderImplementation == "ExternalEncoder") {
										session.pcs[UUID].stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly
										if (session.pcs[UUID].encoder !== true) {
											session.pcs[UUID].encoder = true;
											miniInfo.hw_enc = true;
											sendMini = true;
										}
									} else if (stat.encoderImplementation == "MediaFoundationVideoEncodeAccelerator") {
										session.pcs[UUID].stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly
										if (session.pcs[UUID].encoder !== true) {
											session.pcs[UUID].encoder = true;
											miniInfo.hw_enc = true;
											sendMini = true;
										}
									} else {
										if (session.pcs[UUID].encoder === true) {
											session.pcs[UUID].encoder = false;
											miniInfo.hw_enc = false;
											sendMini = true;
										}
									}
								}

								if ("qualityLimitationReason" in stat) {
									if (session.pcs[UUID].stats.quality_limitation_reason) {
										if (session.pcs[UUID].stats.quality_limitation_reason !== stat.qualityLimitationReason) {
											try {
												sendMini = true;
												miniInfo.qlr = stat.qualityLimitationReason;
											} catch (e) {
												warnlog(e);
											}
										}
									}
									session.pcs[UUID].stats.quality_limitation_reason = stat.qualityLimitationReason;
								}

								if (sendMini) {
									session.sendMessage({ miniInfo: miniInfo }, UUID);
								}

								if ("bytesSent" in stat) {
									if ("_bytesSentVideo" in session.pcs[UUID].stats) {
										if (session.pcs[UUID].stats._timestamp1) {
											if (stat.timestamp) {
												session.pcs[UUID].stats.video_bitrate_kbps = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSentVideo)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1));
											}
										}
									}
									session.pcs[UUID].stats._bytesSentVideo = stat.bytesSent;
								}

								if ("nackCount" in stat) {
									if ("_nackCount" in session.pcs[UUID].stats) {
										if (session.pcs[UUID].stats._timestamp1) {
											if (stat.timestamp) {
												session.pcs[UUID].stats.nacks_per_second = parseInt((10000 * (stat.nackCount - session.pcs[UUID].stats._nackCount)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1)) / 10;
											}
										}
									}
								}
								if ("retransmittedBytesSent" in stat) {
									if ("_retransmittedBytesSent" in session.pcs[UUID].stats) {
										if (session.pcs[UUID].stats._timestamp1) {
											if (stat.timestamp) {
												session.pcs[UUID].stats.retransmitted_kbps = parseInt((8 * (stat.retransmittedBytesSent - session.pcs[UUID].stats._retransmittedBytesSent)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1));
											}
										}
									}
								}

								if ("nackCount" in stat) {
									session.pcs[UUID].stats._nackCount = stat.nackCount;
								}

								if ("retransmittedBytesSent" in stat) {
									session.pcs[UUID].stats._retransmittedBytesSent = stat.retransmittedBytesSent;
								}

								if ("timestamp" in stat) {
									session.pcs[UUID].stats._timestamp1 = stat.timestamp;
								}

								if ("pliCount" in stat) {
									session.pcs[UUID].stats.total_pli_count = stat.pliCount;
								}
								if ("keyFramesEncoded" in stat) {
									session.pcs[UUID].stats.total_key_frames_encoded = stat.keyFramesEncoded;
								}
							} else if (stat.kind == "audio") {
								if ("bytesSent" in stat) {
									if (session.pcs[UUID].stats._bytesSentAudio) {
										if (session.pcs[UUID].stats._timestamp2) {
											if (stat.timestamp) {
												if ("audio_bitrate_kbps" in session.pcs[UUID].stats) {
													session.pcs[UUID].stats.audio_bitrate_kbps += parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSentAudio)) / (stat.timestamp - session.pcs[UUID].stats._timestamp2));
												} else {
													session.pcs[UUID].stats.audio_bitrate_kbps = 0;
												}
											}
										}
									}
								}
								if ("timestamp" in stat) {
									session.pcs[UUID].stats._timestamp2 = stat.timestamp;
								}

								if ("bytesSent" in stat) {
									session.pcs[UUID].stats._bytesSentAudio = stat.bytesSent;
								}
							}
						} else if (stat.type == "remote-candidate") {
							candidates[stat.id] = stat;
							if (stat.candidateType != "relay") {
								ipleakingAllowedRemote = true;
							}
						} else if (stat.type == "local-candidate") {
							candidates[stat.id] = stat;
							if (stat.candidateType != "relay") {
								ipleakingAllowedLocal = true;
							}
						} else if (stat.type == "candidate-pair" && stat.nominated) {
							if (!nominatedCandidate) {
								nominatedCandidate = stat;
							} else if (nominatedCandidate.priority < stat.priority) {
								nominatedCandidate = stat;
							}
						} else if ("mimeType" in stat && "type" in stat && stat.type == "codec") {
							if (stat.mimeType.includes("video")) {
								session.pcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1];
							} else if (stat.mimeType.includes("audio")) {
								session.pcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1];
								if (stat.clockRate) {
									session.pcs[UUID].stats.audio_clockRate = stat.clockRate;
									if (stat.channels) {
										session.pcs[UUID].stats.audio_clockRate += " / " + stat.channels;
									}
								} else if (stat.sdpFmtpLine) {
									session.pcs[UUID].stats.fmtp = stat.sdpFmtpLine;
								}
							}
						}
						return;
					});

					if (nominatedCandidate) {
						if ("availableOutgoingBitrate" in nominatedCandidate) {
							session.pcs[UUID].stats.available_outgoing_bitrate_kbps = parseInt(nominatedCandidate.availableOutgoingBitrate / 1024);
							if (session.maxBandwidth !== false) {
								session.limitMaxBandwidth(session.pcs[UUID].stats.available_outgoing_bitrate_kbps, session.pcs[UUID], false);
							}
						}
						if ("totalRoundTripTime" in nominatedCandidate) {
							if ("responsesReceived" in nominatedCandidate) {
								session.pcs[UUID].stats.average_roundTripTime_ms = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000);
							}
						}
					}
					if (nominatedCandidate && nominatedCandidate.remoteCandidateId) {
						if (candidates[nominatedCandidate.remoteCandidateId]) {
							var candidate = candidates[nominatedCandidate.remoteCandidateId];
							if ("candidateType" in candidate) {
								session.pcs[UUID].stats.candidateType_remote = candidate.candidateType;
								if (candidate.candidateType === "relay") {
									if ("ip" in candidate) {
										session.pcs[UUID].stats.remote_relay_IP = candidate.ip;
									}
									if ("relayProtocol" in candidate) {
										session.pcs[UUID].stats.remote_relay_protocol = candidate.relayProtocol;
									}

									session.pcs[UUID].stats.remote_ip_blocking = !ipleakingAllowedRemote;
								} else {
									try {
										delete session.pcs[UUID].stats.remote_relay_IP;
										delete session.pcs[UUID].stats.remote_relay_protocol;
										delete session.pcs[UUID].stats.remote_ip_blocking;
									} catch (e) {}
								}
							}
						}
					}
					if (nominatedCandidate && nominatedCandidate.localCandidateId) {
						if (candidates[nominatedCandidate.localCandidateId]) {
							var candidate = candidates[nominatedCandidate.localCandidateId];
							if ("candidateType" in candidate) {
								session.pcs[UUID].stats.candidateType_local = candidate.candidateType;

								if (candidate.candidateType === "relay") {
									if ("ip" in candidate) {
										session.pcs[UUID].stats.local_relay_IP = candidate.ip;
									}
									if ("relayProtocol" in candidate) {
										session.pcs[UUID].stats.local_relay_protocol = candidate.relayProtocol;
									}

									session.pcs[UUID].stats.local_ip_blocking = !ipleakingAllowedLocal;
								} else {
									try {
										delete session.pcs[UUID].stats.local_relay_IP;
										delete session.pcs[UUID].stats.local_relay_protocol;
										delete session.pcs[UUID].stats.local_ip_blocking;
									} catch (e) {}
								}
							}
						}
					}

					return;
				});
			},
			0,
			uuid
		);
	}

	try {
		var totalCon = Object.keys(session.pcs).length || 0;
		var headerStats = "<span title='Number of outbound connections'>🔗 ";
		headerStats += totalCon;
		if (meshcastActive) {
			if (totalAudio) {
				headerStats += "</span>, <span title='Number of outbound audio streams'>👂 " + totalAudio;
			}
			if (totalVideo) {
				headerStats += "</span>, <span title='Number of outbound video streams'>👀 " + totalVideo;
			}
			headerStats += "</span>, <span title='Publishing to a Meshcast broadcasting server''>📡Broadcast";
		} else {
			headerStats += "</span>, <span title='Number of outbound audio streams'>👂 " + totalAudio;
			headerStats += "</span>, <span title='Number of outbound video streams'>👀 " + totalVideo;
		}
		if (session.roomid) {
			headerStats += "</span>, <span title='Number of scenes.'>🎬 " + totalScenes + "</span>";
		}

		var changed = false;
		if (!session.info.out) {
			session.info.out = {};
			session.info.out.v = totalVideo;
			session.info.out.a = totalAudio;
			session.info.out.c = totalCon;
			session.info.out.s = totalScenes;
			changed = true;
		} else {
			if (session.info.out.a !== totalAudio) {
				session.info.out.a = totalAudio;
				//	changed = true;  // I'm not sending this data, so why bother
			}
			if (session.info.out.v !== totalVideo) {
				session.info.out.v = totalAudio;
				//changed = true; // I'm not sending this data, so why bother
			}
			if (session.info.out.c !== totalCon) {
				if (session.info.out.c) {
					changed = true; // update if I'm not the first one
				}
				session.info.out.c = totalCon;
			}
			if (session.info.out.s !== totalScenes) {
				if (session.info.out.s) {
					changed = true; // update if I'm not the first one
				}
				session.info.out.s = totalScenes;
			}
		}
	} catch (e) {}
	//session.info.out = {};

	var uploadQuality = nackRate / totalStreams || 0;
	if (totalStreams === 0) {
		uploadQuality = "title='Connection seems good' style='color: transparent; text-shadow: 0 0 0 #4d9bff;'";
	} else if (uploadQuality === 0) {
		uploadQuality = "title='Connection seems good' style='color: transparent; text-shadow: 0 0 0 #0F0;'";
	} else if (uploadQuality <= 1) {
		uploadQuality = "title='Mild connection issues' style='color: transparent; text-shadow: 0 0 0 yellow;'";
	} else if (uploadQuality <= 5) {
		uploadQuality = "title='Moderate connection issues' style='color: transparent; text-shadow: 0 0 0 orange;'";
	} else {
		uploadQuality = "title='Severe connection issues' style='color: transparent; text-shadow: 0 0 0 #F00;'";
	}

	if (Firefox && totalBitrate === 0 && totalBitrate2 === 0) {
		// does not support the current stats system
	} else if (totalBitrate > totalBitrate2) {
		headerStats += ", <span title='Video+Audio upload bitrate'><span " + uploadQuality + ">🔺</span> " + Math.round(totalBitrate / 10.24) / 100 + "<small>-Mbps</small></span>";
	} else if (totalBitrate2 > 1000) {
		headerStats += ", <span title='Total upload bitrate' <span " + uploadQuality + ">🔺</span> " + Math.round(totalBitrate2 / 10.24) / 100 + "<small>-Mbps</small></span>";
	} else {
		headerStats += ", <span title='Total upload bitrate' <span " + uploadQuality + ">🔺</span> " + totalBitrate2 + "<small>-kbps</small></span>";
	}

	if (session.director || !session.roomid) {
		// show stats if the director or if not in a group room
		if (cpuLimited) {
			headerStats += ", <span style='color: #e69a0f;' title='Your CPU is maxed out; this can cause audio, sync, and quality issues.'>🔥 CPU Overloaded</span>";
		}
		//if (relayUsed){
		//	headerStats += " <span title='A relay-server is being used, which can limit this connection's quality.'>💸</span>";
		//}

		directorGraphStats();
	}

	var miniInfo = {};
	if (changed) {
		miniInfo.out = {};
		miniInfo.out.c = session.info.out.c;
	}

	if (session.cpuLimited !== cpuLimited) {
		session.cpuLimited = cpuLimited;
		miniInfo.cpu = cpuLimited;
		changed = true;
	}

	if (changed) {
		for (var uuid in session.pcs) {
			session.sendMessage({ miniInfo: miniInfo }, uuid); // lets send it to everyone.
		}
	}

	if (document.getElementById("head5")) {
		try {
			if (Object.keys(session.pcs).length || meshcastActive) {
				document.getElementById("head5").classList.remove("hidden");
			}
		} catch (e) {}
		document.getElementById("head5").innerHTML = headerStats;
		//miniTranslate(document.getElementById("head5"));
		document.getElementById("head5").onclick = function () {
			var [menu, innerMenu] = statsMenuCreator();
			menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);
			printMyStats(innerMenu);
		};
	}
}

function updateStats(obsvc = false) {
	if (document.getElementById("previewWebcam")) {
		var ele = document.getElementById("previewWebcam");
		var wcs = "webcamstats";
	} else if (document.getElementById("videosource")) {
		var ele = document.getElementById("videosource");
		var wcs = "webcamstats3";
	} else {
		return;
	}

	try {
		getById(wcs).innerHTML = "";
		ele.srcObject.getVideoTracks().forEach(function (track) {
			if (obsvc && parseInt(track.getSettings().frameRate) == 30) {
				getById(wcs).innerHTML = "Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + " @ up to 60fps";
			} else {
				var frameRateFPS = track.getSettings().frameRate;
				if (frameRateFPS) {
					getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + "@" + parseInt(frameRateFPS * 100) / 100.0 + "fps";
				} else {
					getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0);
				}
			}
		});
	} catch (e) {
		errorlog(e);
	}
}

function toggleControlBar() {
	if (!getById("controlButtons").classList.contains("hidden")) {
		getById("controlButtons").dataset.enabled = true;
		getById("controlButtons").classList.add("hidden");
	} else if (getById("controlButtons").dataset.enabled) {
		getById("controlButtons").classList.remove("hidden");
		delete getById("controlButtons").dataset.enabled;
	}
}

function toggleMute(apply = false, event = false) {
	// TODO: I need to have this be MUTE, toggle, with volume not touched.

	var mouseUp = null;
	var touchEnd = null;
	var timeStart = Date.now();
	if (event) {
		mouseUp = document.onmouseup;
		touchEnd = document.ontouchend;
		document.onmouseup = function () {
			document.onmouseup = mouseUp;
			document.ontouchend = touchEnd;
			if (Date.now() - timeStart < 500) {
				return;
			} else {
				toggleMute();
			}
		};
		document.ontouchend = function () {
			document.onmouseup = mouseUp;
			document.ontouchend = touchEnd;
			if (Date.now() - timeStart < 300) {
				return;
			} else {
				toggleMute();
			}
		};
	}

	if (session.director) {
		if (!session.directorEnabledPPT) {
			log("Director doesn't have PPT enabled yet");
			// director has not enabled PTT yet.
			return;
		}
	}

	if (apply) {
		session.muted = !session.muted; // we flip here as we are going to flip again in a second.
	}
	//try{var ptt = getById("press2talk");} catch(e){var ptt=false;}

	if (session.muted == false) {
		session.muted = true;
		getById("mutetoggle").className = "las la-microphone-slash toggleSize";
		if (!session.cleanOutput) {
			getById("mutebutton").classList.add("red", "pulsate");
			getById("mutebutton").ariaPressed = "true";
			getById("header").classList.add("red");

			if (session.localMuteElement) {
				session.localMuteElement.style.display = "block";
			}
		}
		if (session.streamSrc) {
			session.streamSrc.getAudioTracks().forEach(track => {
				track.enabled = false;
			});
		}
		if ((window.obsstudio || session.mobile) && session.videoElement && session.videoElement.srcObject) {
			session.videoElement.srcObject.getAudioTracks().forEach(track => {
				track.enabled = false;
			});
		}
	} else {
		session.muted = false;
		getById("mutetoggle").className = "las la-microphone toggleSize";
		if (!session.cleanOutput) {
			getById("mutebutton").classList.remove("red", "pulsate");
			getById("mutebutton").ariaPressed = "false";
			getById("header").classList.remove("red");

			if (session.localMuteElement) {
				session.localMuteElement.style.display = "none";
			}
		}
		if (session.streamSrc) {
			session.streamSrc.getAudioTracks().forEach(track => {
				track.enabled = true;
			});
		}
		//if (session.mobile) {
			if (session.videoElement && session.videoElement.srcObject) {
				session.videoElement.srcObject.getAudioTracks().forEach(track => {
					track.enabled = true;
				});
			}
			
			if (!apply && event && (iOS || iPad || SafariVersion)){ // manually refreshing the mic
				refreshMicrophoneDevice(); // to address an issue with iOS/iPad devices losing audio when an inbound audio souce hits.
			}
		//}

		// toggleMute(false, event)

		//if (ptt){
		//	ptt.innerHTML = "<span data-translate='Push-to-Mute'>🔴 Push to Mute</span>";
		//}
	}

	try {
		postMessageIframe(document.getElementById("screensharesource"), { mic: !session.muted });
	} catch (e) {}

	if (!apply) {
		// only if they are changing states do we bother to spam.
		var data = {};
		data.muteState = session.muted;
		session.sendMessage(data);
		log("SEND MUTE STATE TO PEERS");
		pokeIframeAPI("mic-mute-state", session.muted);
		pokeAPI("muted", session.muted);
	}
}

function postMessageIframe(iFrameEle, message) {
	// iframes seem to only have the contentWindow work on the last placed iframe object, so this checks the dom first.
	if (iFrameEle && iFrameEle.nodeName == "IFRAME") {
		try {
			if (iFrameEle.id && document.getElementById(iFrameEle.id)) {
				document.getElementById(iFrameEle.id).contentWindow.postMessage(message, "*");
			} else {
				iFrameEle.contentWindow.postMessage(message, "*");
			}
		} catch (e) {
			errorlog(e);
		}
	}
}

function toggleSpeakerMute(apply = false) {
	// TODO: I need to have this be MUTE, toggle, with volume not touched.

	if (CtrlPressed) {
		resetupAudioOut();
	}

	if (apply) {
		session.speakerMuted = !session.speakerMuted;
	}
	if (session.speakerMuted == false) {
		// mute output
		session.speakerMuted = true;
		getById("mutespeakertoggle").className = "las la-volume-mute toggleSize";
		if (!session.cleanOutput) {
			getById("mutespeakerbutton").className = "float red";
		}
		var sounds = document.getElementsByTagName("video");

		if (iOS || iPad) {
			for (var i = 0; i < sounds.length; ++i) {
				if (sounds[i].id === "keepAlivePlayer") {
					// we need to keep this unmuted
					continue;
				}
				sounds[i].muted = !sounds[i].muted;
				sounds[i].muted = session.speakerMuted;
			}
		} else {
			for (var i = 0; i < sounds.length; ++i) {
				if (sounds[i].id === "keepAlivePlayer") {
					// we need to keep this unmuted
					continue;
				}
				sounds[i].muted = session.speakerMuted;
			}
		}
	} else {
		session.speakerMuted = false; // unmute output

		getById("mutespeakertoggle").className = "las la-volume-up toggleSize";
		if (!session.cleanOutput) {
			getById("mutespeakerbutton").className = "float";
		}
		var sounds = document.getElementsByTagName("video");

		if (iOS || iPad) {
			// attempting to fix an iOS bug
			for (var i = 0; i < sounds.length; ++i) {
				sounds[i].muted = !sounds[i].muted;
				if (sounds[i].id === "videosource") {
					// don't unmute ourselves. feedback galore if so.
					sounds[i].muted = true;
					continue;
				} else if (sounds[i].id === "previewWebcam") {
					sounds[i].muted = true;
					continue;
				} else if (sounds[i].id === "screensharesource") {
					sounds[i].muted = true;
					continue;
				} else if (sounds[i].id === "screenshare") {
					// this is a webcam
					sounds[i].muted = true;
					continue;
				} else {
					sounds[i].muted = session.speakerMuted;
				}
			}
		} else {
			for (var i = 0; i < sounds.length; ++i) {
				if (sounds[i].id === "videosource") {
					// don't unmute ourselves. feedback galore if so.
					continue;
				} else if (sounds[i].id === "screensharesource") {
					// don't unmute ourselves. feedback galore if so.
					continue;
				} else if (sounds[i].id === "previewWebcam") {
					continue;
				} else if (sounds[i].id === "screenshare") {
					// this is a webm
					continue;
				} else {
					sounds[i].muted = session.speakerMuted;
				}
			}
		}
	}

	for (var UUID in session.rpcs) {
		applyMuteState(UUID);
		postMessageIframe(session.rpcs[UUID].iframeEle, { mute: session.speakerMuted });
	}

	pokeIframeAPI("audio-mute-state", session.speakerMuted);

	if (!apply) {
		pokeAPI("speakerMuted", session.speakerMuted);
	}

	if (iOS || iPad) {
		resetupAudioOut();
	}
}

const fileTransfers = {};

function toggleFileshare(UUID = false, event = null) {
	if (session.cleanOputput){return;}

    let string = '';
    if (UUID === false) {
        string = 'Share a file with the group<br /><input id="fileselector4" onchange="session.shareFile(this, false, event);" type="file" title="Transfer any file to the group"/><label for="fileselector4">Choose file(s) to share</label>';
    } else if (session.directorList.indexOf(UUID) >= 0) {
        string = `The director requested you share a file with them.<br /><input id="fileselector4" onchange="session.shareFile(this, '${UUID}', event);" type="file" title="Transfer a file to the director"/><label for="fileselector4">Choose file(s) to share</label>`;
    } else {
        string = `Someone has requested you share a file with them.<br /><input id="fileselector4" onchange="session.shareFile(this, '${UUID}', event);" type="file" title="Transfer a file to person"/><label for="fileselector4">Choose file(s) to share</label>`;
    }

    warnUser(string, false, false);
	
	updateFileShare();
}

const enhancedFileTransfers = {
    transfers: {},
    
    initTransfer(fileId, total) {
        if (!this.transfers[fileId]) {
            this.transfers[fileId] = {
                progress: 0,
                total,
                speed: 0,
                lastUpdate: Date.now(),
                bytesLastSecond: 0,
                downloaders: new Map()
            };
        }
    },
    
    updateProgress(fileId, transferred, UUID) {
        const transfer = this.transfers[fileId];
        if (!transfer) return;
        
        const now = Date.now();
        const timeDiff = (now - transfer.lastUpdate) / 1000;
        
        if (UUID && transfer.downloaders.get(UUID)) {
            transfer.downloaders.set(UUID, {
                progress: (transferred / transfer.total) * 100,
                speed: timeDiff >= 1 ? Math.round((transferred - (transfer.downloaders.get(UUID).bytesTransferred || 0)) / timeDiff) : 0,
                bytesTransferred: transferred,
                lastUpdate: now
            });
        }
        
        if (timeDiff >= 1) {
            transfer.speed = Math.round((transferred - transfer.bytesLastSecond) / timeDiff);
            transfer.lastUpdate = now;
            transfer.bytesLastSecond = transferred;
        }
        
        transfer.progress = (transferred / transfer.total) * 100;
        updateFileShare();
    },
    
    removeDownloader(fileId, UUID) {
        const transfer = this.transfers[fileId];
        if (transfer) {
            transfer.downloaders.delete(UUID);
            updateFileShare();
        }
    }
};
let isFileManagerMinimized = false;

function toggleFileManagerSize() {
    isFileManagerMinimized = !isFileManagerMinimized;
    updateFileShare();
}
function updateFileShare() {
    if (session.cleanOutput) return;
    
    const activeSharesDiv = document.getElementById('activeShares');
    activeSharesDiv.innerHTML = '';
    
    const container = document.createElement('div');
    container.className = `file-manager${isFileManagerMinimized ? ' minimized' : ''}`;
    
    // Header
    const header = document.createElement('div');
    header.className = 'file-manager-header';
    const headerControls = document.createElement('div');
    headerControls.className = 'header-controls';
    
    header.innerHTML = `<h3>File Sharing</h3>`;
    headerControls.innerHTML = `
        <button class="button button-primary" onclick="document.getElementById('fileInput').click()">
            Add Files
        </button>
        <button class="button button-secondary" onclick="toggleFileManagerSize()">
            ${isFileManagerMinimized ? 'Expand' : 'Minimize'}
        </button>
    `;
    header.appendChild(headerControls);
    container.appendChild(header);
    
    if (!isFileManagerMinimized) {
        // Hidden file input
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.id = 'fileInput';
        fileInput.style.display = 'none';
        fileInput.multiple = true;
        fileInput.onchange = (e) => session.shareFile(e.target, false, e);
        container.appendChild(fileInput);
        
        // File list
        const fileList = document.createElement('div');
        fileList.className = 'file-list';
        
        if (!(session.hostedFiles && session.hostedFiles.length)) {
            fileList.innerHTML = '<div class="no-files">No files being shared</div>';
        } else {
            session.hostedFiles.forEach(file => {
                const transfer = enhancedFileTransfers.transfers[file.id];
                
                const fileItem = document.createElement('div');
                fileItem.className = 'file-item';
                
                const fileInfo = document.createElement('div');
                fileInfo.className = 'file-info';
                fileInfo.innerHTML = `
                    <span class="file-name">${file.name}</span>
                    <span class="file-size">${formatFileSize(file.size)}</span>
                `;
                
                if (transfer && transfer.downloaders.size > 0) {
                    const downloadList = document.createElement('div');
                    downloadList.className = 'transfer-info';
                    
                    transfer.downloaders.forEach((data, UUID) => {
                        const transferItem = document.createElement('div');
                        transferItem.className = 'transfer-item';
                        transferItem.innerHTML = `
                            <div class="download-info">
                                Downloading by: ${getLabelForUUID(UUID)}
                                <div class="file-progress">
                                    <div class="progress-bar" style="width: ${data.progress}%"></div>
                                </div>
                                <span class="progress-text">${Math.round(data.progress)}%</span>
                                <span class="transfer-speed">${formatSpeed(data.speed)}</span>
                            </div>
                        `;
                        downloadList.appendChild(transferItem);
                    });
                    
                    fileInfo.appendChild(downloadList);
                }
                
                const actions = document.createElement('div');
                actions.innerHTML = `
                    <button class="button button-danger" onclick="cancelFileTransfer('${file.id}')">
                        Stop Sharing
                    </button>
                `;
                
                fileItem.appendChild(fileInfo);
                fileItem.appendChild(actions);
                fileList.appendChild(fileItem);
            });
        }
        
        container.appendChild(fileList);
    }
    
    activeSharesDiv.appendChild(container);
}
function getLabelForUUID(UUID) {
    if (!UUID) return 'Unknown';
    
    let label = UUID;
    if (session.rpcs[UUID] && session.rpcs[UUID].label) {
        label = sanitizeLabel(session.rpcs[UUID].label);
    } else if (session.pcs[UUID] && session.pcs[UUID].label) {
        label = sanitizeLabel(session.pcs[UUID].label);
    }
    
    if (session.directorList.indexOf(UUID) >= 0) {
        label = 'Director: ' + label;
    }
    
    return label;
}
function formatFileSize(bytes) {
    if (bytes === 0) return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}

function formatSpeed(bytesPerSecond) {
    return bytesPerSecond ? formatFileSize(bytesPerSecond) + '/s' : '';
}

function cancelFileTransfer(fileId) {
	console.log(fileId);
	
    if (fileTransfers[fileId]) {
        fileTransfers[fileId].channel.close();
        delete fileTransfers[fileId];
    }
    // Remove the file from hostedFiles array
    session.hostedFiles = session.hostedFiles.filter(file => file.id !== fileId);
    updateFileShare(); // Refresh the file share display
}

function truncateUrl(url) {
    try {
        const urlObj = new URL(url);
        const path = urlObj.pathname.length > 15 ? 
            urlObj.pathname.substring(0, 15) + '...' : 
            urlObj.pathname;
        return urlObj.hostname + path;
    } catch(e) {
        return url.length > 30 ? url.substring(0, 30) + '...' : url;
    }
}

session.sendFile = function (UUID, fileid) {
    log("SENDING FILE: " + fileid + " " + UUID);
    var fr = new FileReader();
    var fid = session.hostedFiles.findIndex(file => file.id === fileid);
    
    if (fid === -1) {
        warnlog("requested file was not found");
        return;
    } else if (session.hostedFiles[fid].state == 0) {
        warnlog("requested file has been removed.");
        return;
    } else if (session.hostedFiles[fid].restricted && session.hostedFiles[fid].restricted !== UUID) {
        warnlog("user didn't have access for this file.");
        return;
    }
    
    var chunksize = 16384;
    var cid = 0;
    var channelName = fileid;
    if (channelName === "sendChannel") {
        channelName = "sendChannel_" + session.generateStreamID(5);
    }
    
    var transferchannel;
    if (UUID in session.pcs) {
        transferchannel = session.pcs[UUID].createDataChannel(channelName);
    } else if (UUID in session.rpcs) {
        transferchannel = session.rpcs[UUID].createDataChannel(channelName);
    } else {
        warnlog("UUID does not exist");
        return;
    }
    
    transferchannel.binaryType = "arraybuffer";
    var chunk = session.hostedFiles[fid].file.slice(0, chunksize);  // Use the stored File object
    
    fileTransfers[fileid] = {
        channel: transferchannel,
        progress: 0
    };
    
    transferchannel.onopen = () => {
        transferchannel.send(JSON.stringify({ type: "filetransfer", size: session.hostedFiles[fid].size, filename: session.hostedFiles[fid].name, id: session.hostedFiles[fid].id }));
        fr.readAsArrayBuffer(chunk);
    };
    
    transferchannel.onclose = () => {
        try {
            var index = session.hostedTransfers.indexOf(transferchannel);
            if (index > -1) {
                session.hostedTransfers.splice(index, 1);
            }
        } catch (e) {
            errorlog(e);
        }
        log("Transfer ended");
        delete fileTransfers[fileid];
        updateFileShare(); // Refresh the file share display
        transferchannel = null;
		enhancedFileTransfers.removeDownloader(fileid, UUID);
    };
    
    transferchannel.onmessage = event => {
		// console.log(event.data);
	};

	session.hostedTransfers.push(transferchannel);
	
    
    fr.onload = function () {
        if (session.hostedFiles[fid].state == 0) {
            return;
        }
        var arrayBuffer = fr.result;
        try {
            transferchannel.send(arrayBuffer);
        } catch (e) {
            try {
                transferchannel.close();
            } catch (e) {}
            warnlog(e);
            return;
        }
        cid += 1;
        const progress = Math.min(100, Math.round((cid * chunksize / session.hostedFiles[fid].size) * 100));
        fileTransfers[fileid].progress = progress;
        updateFileShare(); // Update progress display
		
		enhancedFileTransfers.updateProgress(fileid, cid * chunksize, UUID);
        
        if (cid * chunksize < session.hostedFiles[fid].size) {
            try {
                chunk = session.hostedFiles[fid].file.slice(cid * chunksize, (cid + 1) * chunksize);  // Use the stored File object
                fr.readAsArrayBuffer(chunk);
            } catch (e) {
                errorlog(e);
            }
        } else {
            transferchannel.send("EOF1");
            transferchannel.close();
        }
    };
};

function toggleChat(event = null) {
    const chatModule = document.getElementById('chatModule'); 
	
	if (!chatModule.configured){
		setupAdjustableChat();
	}
	
    if (session.chat === false) {
        session.chat = true;
        chatModule.classList.remove("hidden");
        getById("chatInput").focus();
		getById("chatNotification").classList.remove("notification", "red");
    } else {
        session.chat = false;
        chatModule.classList.add("hidden");
    }
    updateMessages();
}

function toggleDirectFeedback(event = null) {
    const unmuteSelf = document.getElementById('unmuteSelf');
    unmuteSelf.classList.remove("hidden");
	
	if (session.videoElement){
		session.videoElement.muted = session.videoElement.muted ? false : true;
		
		if (session.selfVolume){
			session.selfVolume =  parseFloat(session.selfVolume);
			if (session.selfVolume>=1){
				session.videoElement.volume = Math.min(100,Math.max(1,session.selfVolume))/100;
				session.selfVolume = null;
			} else {
				session.videoElement.volume = Math.min(1,Math.max(0,session.selfVolume));
				session.selfVolume = null;
			}
		}
	}
	
	if (session.videoElement.muted){
		unmuteSelf.classList.remove("red", "pulsate");
		unmuteSelf.ariaPressed = "false";
		
	} else {
		unmuteSelf.classList.add("red", "pulsate");
		unmuteSelf.ariaPressed = "true";
	}
}

function setupAdjustableChat() {
    const chatModule = document.getElementById('chatModule');
    chatModule.configured = true;
    
    const chatBody = getById('chatBody');
    let isResizing = false;
    let isDragging = false;
    let startY, startHeight, startX, startTop, startLeft;

    function initResize(e) {
        isResizing = true;
		document.body.style.userSelect = 'none';
        startY = e.clientY;
        startHeight = parseInt(window.getComputedStyle(chatBody).height, 10);
        document.addEventListener('mousemove', resize);
        document.addEventListener('mouseup', stopResize);
    }

    function resize(e) {
        if (isResizing) {
            const maxHeight = window.innerHeight - chatModule.offsetTop - 20; // 20px buffer
            const newHeight = Math.min(startHeight + (e.clientY - startY), maxHeight);
            chatBody.style.height = `${Math.max(newHeight, 50)}px`; // Minimum height of 50px
			keepInBounds();
        }
    }

    function stopResize() {
        isResizing = false;
        document.removeEventListener('mousemove', resize);
        document.removeEventListener('mouseup', stopResize);
		document.body.style.userSelect = '';
    }

    function initDrag(e) {
        isDragging = true;
		document.body.style.userSelect = 'none';
        startX = e.clientX - chatModule.offsetLeft;
        startY = e.clientY - chatModule.offsetTop;
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', stopDrag);
    }

    function drag(e) {
        if (isDragging) {
            const newLeft = Math.min(Math.max(e.clientX - startX, 0), window.innerWidth - chatModule.offsetWidth);
            const newTop = Math.min(Math.max(e.clientY - startY, 0), window.innerHeight - chatModule.offsetHeight);
            chatModule.style.left = `${newLeft}px`;
            chatModule.style.top = `${newTop}px`;
            chatModule.style.bottom = 'auto';
            chatModule.style.right = 'auto';
			keepInBounds();
        }
    }

    function stopDrag() {
        isDragging = false;
        document.removeEventListener('mousemove', drag);
        document.removeEventListener('mouseup', stopDrag);
		document.body.style.userSelect = '';
    }

    function keepInBounds() {
        const rect = chatModule.getBoundingClientRect();
		
        if (rect.right > window.innerWidth) {
            chatModule.style.left = `${window.innerWidth - rect.width}px`;
        }
        if (rect.bottom > window.innerHeight) {
            chatModule.style.top = `${window.innerHeight - rect.height}px`;
        }
        if (rect.left < 0) {
            chatModule.style.left = '0px';
        }
        if (rect.top < 0) {
            chatModule.style.top = '0px';
            chatModule.style.bottom = 'auto';
        }
    }

    chatModule.querySelector('.resizer').addEventListener('mousedown', initResize);
    chatModule.querySelector('.chat-header').addEventListener('mousedown', initDrag);
    
    // Keep chat window in bounds when window is resized
    window.addEventListener('resize', keepInBounds);
    
    // Initial positioning
    keepInBounds();
}

function directorAdvanced(ele) {
	var target = document.createElement("div");
	target.style = "position:absolute;float:left;width:270px;height:222px;background-color:#7E7E7E;";

	var closeButton = document.createElement("button");
	closeButton.innerHTML = "<i class='las la-times'></i> close";
	closeButton.style.left = "5px";
	closeButton.style.position = "relative";
	closeButton.onclick = function () {
		target.parentNode.removeChild(target);
	};
	target.appendChild(closeButton);

	var someButton = document.createElement("button");
	someButton.innerHTML = "<i class='las la-reply'></i> some action ";
	someButton.style.left = "5px";
	someButton.style.position = "relative";
	someButton.onclick = function () {
		var actionMsg = {};
		session.sendRequest(actionMsg, ele.dataset.UUID);
	};
	target.appendChild(someButton);

	ele.parentNode.appendChild(target);
}

function directorSendMessage(ele) {
	var UUID = ele.dataset.UUID;
	var target = document.querySelector("[data--u-u-i-d='" + UUID + "'][data-action-type='messaging-box']");
	if (!target) {
		return;
	}

	if (target.classList.contains("hidden")) {
		target.classList.remove("hidden");
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
	} else {
		target.classList.add("hidden");
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		return;
	}

	var inputField = target.querySelector("[data-action-type='messaging-box-text']");
	if (inputField) {
		inputField.focus();
		inputField.select();
	}
	if ("overlay" in target) {
		return;
	}

	target.overlay = true;

	if (inputField) {
		inputField.addEventListener("keydown", function (e) {
			if (e.keyCode == 13) {
				e.preventDefault();
				sendButton.click();
			} else if (e.keyCode == 27) {
				e.preventDefault();
				inputField.value = "";
				target.parentNode.removeChild(target);
			}
		});
	}

	var sendButton = target.querySelector("[data-action-type='messaging-box-send']");
	if (sendButton) {
		sendButton.onclick = function () {
			var chatMsg = {};
			chatMsg.chat = inputField.value;
			if (sendButton.parentNode.overlay) {
				chatMsg.overlay = sendButton.parentNode.overlay;
			}
			session.sendRequest(chatMsg, ele.dataset.UUID);
			inputField.value = "";
			//target.parentNode.removeChild(target);
		};
	}

	var closeButton = target.querySelector("[data-action-type='messaging-box-close']");
	if (closeButton) {
		closeButton.onclick = function () {
			inputField.value = "";
			target.classList.add("hidden");
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
		};
	}

	var overlayMsg = target.querySelector("[data-action-type='messaging-box-toggle']");
	if (overlayMsg) {
		overlayMsg.onclick = function (e) {
			log(e.target.parentNode.parentNode);
			if (e.target.parentNode.parentNode.overlay === true) {
				e.target.parentNode.parentNode.overlay = false;
				e.target.parentNode.innerHTML = "<i class='las la-bell-slash' style='font-size:170%; color:#DDD; cursor:pointer;'></i>";
			} else {
				e.target.parentNode.parentNode.overlay = true;
				e.target.parentNode.innerHTML = "<i class='las la-bell' style='font-size:170%; color:#FFF; cursor:pointer;'></i>";
			}
		};
	}
}

function toggleAutoVideoMute() {
	// for iOS devices, that tab out.
	// document.visibilityState
	if (!session.videoMuted && session.permaid !== false) {
		var msg = {};
		msg.videoMuted = document.visibilityState === "hidden" || false;
		//try {
		session.sendMessage(msg);
		//} catch(e){errorlog(e);}
		pokeIframeAPI("video-mute-state", document.visibilityState);
	}
}

function toggleVideoMute(apply = false) {
	// TODO: I need to have this be MUTE, toggle, with volume not touched.
	if (apply) {
		session.videoMuted = !session.videoMuted;
	}

	if (!session.remoteVideoMuted) {
		getById("head8").classList.add("hidden");
	}

	if (session.videoMuted == false) {
		session.videoMuted = true;
		getById("mutevideotoggle").className = "las la-video-slash toggleSize";
		if (!session.cleanOutput) {
			getById("mutevideobutton").classList.add("red");
			getById("mutevideobutton").ariaPressed = "true";
			getById("header").classList.add("red");
			if (session.remoteVideoMuted) {
				getById("head8").classList.remove("hidden");
			}
		}
		if (session.streamSrc) {
			session.streamSrc.getVideoTracks().forEach(track => {
				track.enabled = false;
			});
		}
	} else if (session.remoteVideoMuted) {
		// the director has muted this guest's video feed
		session.videoMuted = false; // just setting it back to the pre-toggled state
		getById("mutevideotoggle").className = "las la-video toggleSize";
		if (!session.cleanOutput) {
			getById("head8").classList.remove("hidden");
			getById("header").classList.add("red");
			getById("mutevideobutton").classList.remove("red");
			getById("mutevideobutton").ariaPressed = "false";
		}
		if (session.streamSrc) {
			session.streamSrc.getVideoTracks().forEach(track => {
				track.enabled = false;
			});
		}
	} else {
		session.videoMuted = false;

		getById("mutevideotoggle").className = "las la-video toggleSize";
		if (!session.cleanOutput) {
			getById("mutevideobutton").classList.remove("red");
			getById("mutevideobutton").ariaPressed = "false";
			getById("header").classList.remove("red");
		}
		if (session.streamSrc) {
			session.streamSrc.getVideoTracks().forEach(track => {
				track.enabled = true;
			});
		}
	}

	if (session.avatar && session.avatar.ready && !apply) {
		updateRenderOutpipe();
		if (session.videoMuted) {
			var msg = {};
			msg.videoMuted = false; // doesn't matter the actual mute state; this is the avatar
			session.sendMessage(msg);
		}
	} else if (!apply) {
		var msg = {};
		msg.videoMuted = session.videoMuted;
		session.sendMessage(msg);
	}

	pokeIframeAPI("video-mute-state", session.videoMuted || session.remoteVideoMuted);

	if (!apply) {
		pokeAPI("videoMuted", session.videoMuted || session.remoteVideoMuted);
	}

	if (session.style && session.style == 1) {
		if (!session.videoElement || session.videoElement.id !== "previewWebcam") {
			updateMixer();
		}
	}
}
var toggleSettingsState = false;
let settingsClickHandler = null;

async function toggleSettings(forceShow = false) {
    if (session.nosettings) return;

    const multiselectTrigger = getById("multiselect-trigger3");
    multiselectTrigger.dataset.state = "0";
    multiselectTrigger.classList.add("closed");
    multiselectTrigger.classList.remove("open");
    getById("chevarrow2").classList.add("bottom");

    const popupSelector = getById("popupSelector");

    // For forceShow, if already open just update devices
    if (toggleSettingsState && forceShow) {
        await enumerateDevices().then(gotDevices2);
        return;
    }

    if (popupSelector.style.display === "none") {
        await showSettings();
    } else {
        hideSettings();
    }

    pokeIframeAPI("settings-menu-state", toggleSettingsState);
}

async function showSettings() {
    const popupSelector = getById("popupSelector");
    const settingsButton = getById("settingsbutton");

    updateConstraintSliders();

    // Handler only closes the menu when clicking outside
    settingsClickHandler = (e) => {
        if (!popupSelector.contains(e.target) && !e.target.closest('#settingsbutton')) {
            hideSettings();
            pokeIframeAPI("settings-menu-state", false);
        }
    };
    
    setTimeout(() => {
        document.addEventListener("click", settingsClickHandler);
    }, 10);

    if (navigator.userAgent.indexOf("Chrome") !== -1) {
        try {
            const permissionResult = await navigator.permissions.query({ name: "camera" });
            if (permissionResult.state === "prompt") {
                try {
                    const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
                    await enumerateDevices().then(gotDevices2);
                    stream.getTracks().forEach(track => track.stop());
                } catch {
                    await enumerateDevices().then(gotDevices2);
                }
            } else {
                await enumerateDevices().then(gotDevices2);
            }
        } catch {
            await enumerateDevices().then(gotDevices2);
        }
    } else {
        await enumerateDevices().then(gotDevices2);
    }

    popupSelector.style.display = "inline-block";
    settingsButton.classList.add("brown");
    settingsButton.ariaPressed = "true";
    
    loadContentEffectsImages();
    
    setTimeout(() => {
        popupSelector.style.right = "0px";
    }, 1);
    
    toggleSettingsState = true;
}

function hideSettings() {
    const popupSelector = getById("popupSelector");
    const settingsButton = getById("settingsbutton");

    if (settingsClickHandler) {
        document.removeEventListener("click", settingsClickHandler);
        settingsClickHandler = null;
    }

    popupSelector.style.right = "-500px";
    settingsButton.classList.remove("brown");
    settingsButton.ariaPressed = "false";
    
    setTimeout(() => {
        popupSelector.style.display = "none";
    }, 200);
    
    toggleSettingsState = false;
    getById("videoSettings3").style.display = "none";
}

let wakeLockObject = null;
async function acquireWakeLock() {
  if ('wakeLock' in navigator) {
	try {
	  wakeLockObject = await navigator.wakeLock.request('screen');
	  log('Wake Lock is active');
	} catch (err) {
	  errorlog(err);
	}
  } else {
	warnlog('Wake Lock API is not supported in this browser');
  }
}
function handleVisibilityChangeWakeLock() {
  if (wakeLockObject !== null && document.visibilityState === 'visible') {
	acquireWakeLock();
  }
}
function releaseWakeLock() {
  if (wakeLockObject) {
	wakeLockObject.release()
	  .then(() => {
		wakeLockObject = null;
		log('Wake Lock is released');
	  })
	  .catch((err) => {
		errorlog(err);
	  });
  }
}

session.hangup = function (reload = false, estop = false) {
	try {
		window.removeEventListener("beforeunload", confirmUnload);
	} catch (e) {}

	try {
		if (estop) {
			recordLocalVideo("estop");
		}
	} catch (e) {}
	try {
		if (estop) {
			recordLocalVideo("estop", false, false, true); // screen share
		}
	} catch (e) {}
	session.taintedSession = true;
	warnlog("hanging up");

	try {
		recordLocalVideo("stop");
	} catch (e) {}
	try {
		recordLocalVideo("stop", false, false, true); // screen share
	} catch (e) {}

	try {
		transferList.forEach(file => {
			if (file.writer) {
				file.writer.close();
			}
			if (file.videoElement && file.videoElement.stopWriter) {
				file.videoElement.stopWriter(true); // estop
			}
		});
	} catch (e) {
		errorlog(e);
	}

	try {
		var msg = {};
		msg.videoMuted = true; // might not trigger
		msg.bye = true;
		session.sendMessage(msg); // make sure the remote video goes black.
	} catch (e) {}

	try {
		session.ws.close();
	} catch (e) {}

	try {
		if (session.canvasSource && session.canvasSource.srcObject) {
			session.canvasSource.srcObject.getTracks().forEach(function (track) {
				session.canvasSource.srcObject.removeTrack(track);
				track.stop();
				log("stopping old track");
			});
		}
		if (session.videoElement && session.videoElement.srcObject) {
			session.videoElement.srcObject.getTracks().forEach(function (track) {
				session.videoElement.srcObject.removeTrack(track);
				track.stop();
				log("stopping old track");
			});
		}
		if (session.streamSrc) {
			session.streamSrc.getTracks().forEach(function (track) {
				session.streamSrc.removeTrack(track);
				track.stop();
				log("stopping old track");
			});
		}
		if (session.streamSrcClone) {
			session.streamSrcClone.getTracks().forEach(function (track) {
				session.streamSrcClone.removeTrack(track);
				track.stop();
				log("stopping old track");
			});
		}
		if (session.screenStream) {
			session.screenStream.getTracks().forEach(function (track) {
				session.screenStream.removeTrack(track);
				track.stop();
				log("stopping old track");
			});
		}
	} catch (e) {
		errorlog(e);
	}
	try {
		for (i in session.rpcs) {
			try {
				if (session.rpcs[i].videoElement) {
					if (session.rpcs[i].videoElement.recording) {
						recordLocalVideo("stop", null, session.rpcs[i].videoElement);
					}
				}
			} catch (e) {}
			log("closing rpc due to hangup event");
			session.closeRPC(i, true);
		}

		for (i in session.pcs) {
			log("closing 5");
			session.closePC(i);
		}
	} catch (e) {
		errorlog(e);
	}

	for (var sid in session.watchTimeoutList) {
		clearTimeout(session.watchTimeoutList[sid]);
	}

	if (session.whipOut && session.whipOut.deleteme) {
		session.whipOut.deleteme();
	}

	if (DebugLog && errorReport) {
		downloadLogs();
	}
	
	if (session.popupChat){
		if (!session.popupChat.closed) {
            session.popupChat.close();
			session.popupChat = null;
        }
	}
	
	releaseWakeLock();

	if (reload) {
		reloadRequested();
		warnlog("Reloading? uh oh. Why didn't it?");
		return;
	} else {
		setTimeout(function () {
			for (i in session) {
				try {
					delete session[i];
				} catch (e) {}
			}
			delete session;
		}, 1200);

		hangupComplete();
		log("HANG UP COMPLETE");
	}
};

function hangup(showhangup = true) {
	// TODO: I need to have this be MUTE, toggle, with volume not touched.
	if (session.hostedTransfers.length) {
		confirmAlt("There are still file transfer in progress\nAre you sure you wish to exit?").then(res => {
			if (res) {
				try {
					if (showhangup) {
						document.getElementById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML;
					} else {
						document.getElementById("main").innerHTML = "";
						document.getElementById("hangupTemplate").innerHTML = "";
					}
				} catch (e) {}

				setTimeout(function () {
					session.hangup();
				}, 0);
			}
		});
	} else {
		try {
			if (showhangup) {
				document.getElementById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML;
			} else {
				document.getElementById("main").innerHTML = "";
				document.getElementById("hangupTemplate").innerHTML = "";
			}
		} catch (e) {}

		setTimeout(function () {
			session.hangup();
		}, 0);
	}
}

function hangup2() {
	session.hangupDirector();
	getById("miniPerformer").innerHTML = "";
	getById("press2talk").dataset.enabled = false;
	getById("screensharebutton").classList.add("hidden");
	getById("screenshare2button").classList.add("hidden");
	getById("screenshare3button").classList.add("hidden");
	getById("settingsbutton").classList.add("hidden");
	getById("mutebutton").classList.add("hidden");
	getById("hangupbutton2").classList.add("hidden");
	//getById("chatbutton").classList.remove("hidden");
	getById("controlButtons").classList.remove("hidden");
	//getById("mutespeakerbutton").classList.add("hidden");
	getById("mutevideobutton").classList.add("hidden");

	getById("screensharebutton").classList.remove("green");
	getById("screensharebutton").ariaPressed = "false";

	if (!session.showDirector) {
		getById("miniPerformer").innerHTML = '<button id="press2talk" onmousedown="event.preventDefault(); event.stopPropagation();" class="float" onclick="press2talk(true);" title="You can also enable the director`s Video Output afterwards by clicking the Setting`s button"><i class="las la-headset"></i><span data-translate="push-to-talk-enable"> enable director`s microphone or video<br />(only guests can see this feed)</span></button>';
		miniTranslate(getById("miniPerformer"));
	} else {
		getById("miniPerformer").innerHTML = '<button id="press2talk" onmousedown="event.preventDefault(); event.stopPropagation();" class="float" onclick="press2talk(true);" title="You can also enable the director`s Video Output afterwards by clicking the Setting`s button"><i class="las la-headset"></i><span data-translate="push-to-talk-enable-2"> enable director`s microphone or video</span></button>';
	}
	getById("miniPerformer").className = "";
}

function hangupComplete() {
	try {
		//if (document.fullscreenElement && session.mobile){
		//	getById("main").innerHTML = document.getElementById("hangupTemplateMobileFullscreen").innerHTML;
		//} else {
		getById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML;
		//}
	} catch (e) {}

	updateMixerRun = function () {};

	pokeIframeAPI("hungup", true); // don't use Hangup, as that's an action.
	pokeAPI("hangup", true);
}

function reloadRequested() {
	pokeIframeAPI("reloading", true);
	window.removeEventListener("beforeunload", confirmUnload); // clear the confirm on reload
	location.reload(); // the main reload function call
}
function confirmUnload(event) {
	if (!session.noExitPrompt && !session.cleanOutput && session.scene === false && (session.seeding || session.roomid !== false || session.permaid !== false || session.director)) {
		(event || window.event).returnValue = "Are you sure you want to exit?"; //Gecko + IE
		return "Are you sure you want to exit?";
	} else {
		return undefined; // ADDED OCT 29th; get rid of popup. Just close the socket connection if the user is refreshing the page.  It's one or the other.
	}
}

function gobackSlide() {
	var data = {};
	data.data = [176, 110, 10];
	sendRawMIDI(data);
	try {
		pokeIframeAPI("back-slide", true);
	} catch (e) {}
}

function nextSlide() {
	var data = {};
	data.data = [176, 110, 11];
	sendRawMIDI(data);

	try {
		pokeIframeAPI("next-slide", true);
	} catch (e) {}
}

function raisehand() {
	if (session.directorUUID == false) {
		// fine
		log("no director in room yet");
		return false;
	}

	var data = {};
	var handstate = false;

	log(data);
	if (getById("raisehandbutton").dataset.raised == "0") {
		getById("raisehandbutton").dataset.raised = "1";
		getById("raisehandbutton").classList.add("raisedHand");
		data.chat = "Raised hand";
		handstate = true;
		log("hand raised");
	} else {
		log("hand lowered");
		getById("raisehandbutton").dataset.raised = "0";
		getById("raisehandbutton").classList.remove("raisedHand");
		data.chat = "Lowered hand";
		handstate = false;
	}
	for (var i = 0; i < session.directorList.length; i++) {
		data.UUID = session.directorList[i];
		session.sendMessage(data, data.UUID);
	}

	try {
		pokeIframeAPI("hand", handstate);
	} catch (e) {}

	return handstate;
}

function lowerhand() {
	log("hand lowered");
	getById("raisehandbutton").dataset.raised = "0";
	getById("raisehandbutton").classList.remove("raisedHand");
	pokeIframeAPI("hand", false);
	return false;
}

var previousRoom = "";
var stillNeedRoom = true;
var transferCancelled = false;
var armedTransfer = false;
var transferSettings = {};

async function directMigrate(ele, event, room = false) {
	// everyone in the room will hangup this guest also?  I like that idea.  What about the STREAM ID?  I suppose we don't kick out if the viewID matches.
	log("directMigrate");
	if (room) {
		var migrateRoom = room;
	} else if (event === false) {
		if (previousRoom === null) {
			// user cancelled in previous callback
			ele.innerHTML = '<i class="las la-paper-plane"></i> <span data-translate="forward-to-room">transfer</span>';
			miniTranslate(ele);
			//ele.style.backgroundColor = null;
			ele.classList.remove("armed");
			return false;
		}
		if (transferCancelled === true) {
			ele.innerHTML = '<i class="las la-paper-plane"></i> <span data-translate="forward-to-room">transfer</span>';
			miniTranslate(ele);
			//ele.style.backgroundColor = null;
			ele.classList.remove("armed");
			return false;
		}
		var migrateRoom = previousRoom;
	} else if (event.ctrlKey || event.metaKey) {
		ele.innerHTML = '<i class="las la-check"></i> <span data-translate="forward-to-room">armed</span>';
		miniTranslate(ele);
		ele.classList.add("armed");
		//ele.style.backgroundColor = "#BF3F3F";
		transferCancelled = false;
		//armedTransfer=true;
		Callbacks.push([directMigrate, ele, stillNeedRoom]);
		stillNeedRoom = false;
		log("Migrate queued");
		return true;
		// } else if (armedTransfer){
		//migrateRoom = sanitizeRoomName(previousRoom);
	} else {
		if (armedTransfer !== false && previousRoom !== "") {
			var migrateRoom = sanitizeRoomName(previousRoom);
		} else {
			var broadcastMode = null;
			if ("broadcast" in transferSettings) {
				broadcastMode = transferSettings.broadcast;
			} else if (session.rpcs[ele.dataset.UUID] && session.rpcs[ele.dataset.UUID].stats.info && "broadcast_mode" in session.rpcs[ele.dataset.UUID].stats.info) {
				broadcastMode = session.rpcs[ele.dataset.UUID].stats.info.broadcast_mode;
			} else if (session.broadcastTransfer !== null) {
				broadcastMode = session.broadcastTransfer;
			}

			var queuedMode = null;
			if ("queue" in transferSettings) {
				queuedMode = transferSettings.queue;
			} else if (session.queueTransfer) {
				queuedMode = session.queueTransfer;
			}

			var updateurl = null;
			if ("updateurl" in transferSettings) {
				updateurl = transferSettings.updateurl;
			}
			window.focus();

			var response = await promptTransfer(previousRoom, broadcastMode, updateurl, queuedMode);
			var migrateRoom = response.roomid;
			if (migrateRoom !== null) {
				transferSettings = response;
			}
		}
		stillNeedRoom = true;
		if (migrateRoom === null) {
			// user cancelled
			ele.innerHTML = '<i class="las la-paper-plane"></i> <span data-translate="forward-to-room">transfer</span>';
			miniTranslate(ele);
			//ele.style.backgroundColor = null;
			ele.classList.remove("armed");
			transferCancelled = true;
			return false;
		}
		try {
			migrateRoom = sanitizeRoomName(migrateRoom);
			previousRoom = migrateRoom;
		} catch (e) {}
	}
	ele.innerHTML = '<i class="las la-paper-plane"></i> <span data-translate="forward-to-room">transfer</span>';
	miniTranslate(ele);
	//ele.style.backgroundColor = null;
	ele.classList.remove("armed");

	if (migrateRoom) {
		previousRoom = migrateRoom;
		session.directMigrateIssue(migrateRoom, transferSettings, ele.dataset.UUID);
		return true;
	}
}

var stillNeedHangupTarget = 1;
function directHangup(ele, event) {
	// everyone in the room will hangup this guest?  I like that idea.
	if (event == false) {
		if (stillNeedHangupTarget === 1) {
			window.focus();
			var confirmHangup = confirm(getTranslation("confirm-disconnect-users"));
			stillNeedHangupTarget = confirmHangup;
		} else {
			confirmHangup = stillNeedHangupTarget;
		}
	} else if (event === true) {
		var confirmHangup = true;
	} else if (event.ctrlKey || event.metaKey) {
		ele.innerHTML = '<i class="las la-skull-crossbones"></i> <span data-translate="disconnect-guest" >ARMED</span>';
		miniTranslate(ele);
		ele.classList.add("armed");
		//ele.style.backgroundColor = "#BF3F3F";
		stillNeedHangupTarget = 1;
		Callbacks.push([directHangup, ele, false]);
		log("Hangup queued");
		return;
	} else {
		window.focus();
		var confirmHangup = confirm(getTranslation("confirm-disconnect-user"));
	}

	if (confirmHangup) {
		var msg = {};
		msg.hangup = true;
		log(msg);
		log(ele.dataset.UUID);
		session.sendRequest(msg, ele.dataset.UUID);
		pokeIframeAPI("hungup", "directing", ele.dataset.UUID);
		//session.anysend(msg); // send to everyone in the room, so they know if they are on air or not.
		return true;
	} else {
		ele.innerHTML = '<i class="las la-sign-out-alt"></i><span data-translate="disconnect-guest"> Hangup</span>';
		miniTranslate(ele);
		//ele.style.backgroundColor = null;
		ele.classList.remove("armed");
		return false;
	}
}

async function directAudioChannel(ele, event, director=false) {
	var UUID = ele.dataset.UUID;
	var channel = parseInt(ele.dataset.channel);
	var added = false;
	
	
	if (!(event.ctrlKey || event.metaKey)) {
		if (ele.dataset.state == "1") {
			ele.dataset.state = "0";
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
		} else {
			if (channel == 4){
				if (event){
					let ret = await confirmAlt("⚠️Warning! Channel 4 should be avoided.\n\nChannel 4 in OBS is used for low-frequency audio and may distort the audio if you record to it.\n\nDo you wish to proceed?", false);
					if (!ret){
						return;
					}
				}
			}
			ele.dataset.state = "1"
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			added = true;
		}
	} else if (ele.dataset.state == "1") {
		added = true;
	}
	
	if (director){
		
		if (added){
			document.querySelectorAll('#controls_director [data-action-type="sceneAudioChannel"][data-state="1"]:not([data-channel="'+channel+'"])').forEach(el=>{
				el.dataset.state = "0";
				el.classList.remove("pressed");
				el.ariaPressed = "false";
			});
		} else {
			document.querySelectorAll('#controls_director [data-action-type="sceneAudioChannel"][data-state="1"][data-channel]').forEach(el=>{
				el.dataset.state = "0";
				el.classList.remove("pressed");
				el.ariaPressed = "false";
			});
			channel = false;
		}

	} else {
	
		if (added){
			document.querySelectorAll('[data-sid][data--u-u-i-d="'+UUID+'"][data-action-type="sceneAudioChannel"][data-state="1"]:not([data-channel="'+channel+'"])').forEach(el=>{
				el.dataset.state = "0";
				el.classList.remove("pressed");
				el.ariaPressed = "false";
			});
		} else {
			document.querySelectorAll('[data-sid][data--u-u-i-d="'+UUID+'"][data-channel][data-action-type="sceneAudioChannel"][data-state="1"]').forEach(el=>{
				el.dataset.state = "0";
				el.classList.remove("pressed");
				el.ariaPressed = "false";
			});
			channel = false;
		}
	}
	

	var msg = {};

	msg.audioOutputChannel = channel;
	if (director){
		msg.sid = session.streamID;
	} else {
		msg.sid = ele.dataset.sid;
	}

	for (var uuid in session.pcs) {
		if (session.pcs[uuid].scene !== false) {
			session.sendMessage(msg, uuid);
		}
	}
	syncDirectorState(ele);

	if (channel) {
		return true;
	} else {
		return false;
	}
}

function directEnable(ele, event, director = false) {
	// A directing room only is controlled by the Director, with the exception of MUTE.
	var scene = ele.dataset.scene;
	if (!(event.ctrlKey || event.metaKey)) {
		if (ele.value == 1) {
			ele.value = 0;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			if (ele.children[1]) {
				ele.children[1].innerHTML = "Add to Scene " + scene;
			}
			if (director) {
				var cc = 0;
				getById("container_director")
					.querySelectorAll('[data-action-type="addToScene"]')
					.forEach(ge => {
						if (ge.value == 1) {
							cc += 1;
						}
					});
				if (!cc) {
					getById("container_director").style.backgroundColor = null;
					getById("container_director").classList.remove("containerGreen");
				}
			} else {
				var cc = 0;
				getById("container_" + ele.dataset.UUID)
					.querySelectorAll('[data-action-type="addToScene"]')
					.forEach(ge => {
						if (ge.value == 1) {
							cc += 1;
							log("ge.value: '" + ge.value + "'");
						} else {
							log("ge.value:--'" + ge.value + "'");
						}
					});
				log(cc + " " + "container_" + ele.dataset.UUID);
				if (!cc) {
					getById("container_" + ele.dataset.UUID).style.backgroundColor = null;
					getById("container_" + ele.dataset.UUID).classList.remove("containerGreen");
				}
			}
		} else {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			if (ele.children[1]) {
				ele.children[1].innerHTML = "Remove";
			}
			if (director) {
				getById("container_director").classList.add("containerGreen");
			} else {
				getById("container_" + ele.dataset.UUID).classList.add("containerGreen");
			}
		}
	}

	var msg = {};

	scene = scene + "";

	msg.scene = scene;
	msg.action = "display";
	msg.value = ele.value;
	msg.target = ele.dataset.sid;

	try {
		if (msg.value == 1) {
			pokeIframeAPI("add-to-scene", scene, ele.dataset.UUID);
		} else {
			pokeIframeAPI("remove-from-scene", scene, ele.dataset.UUID);
		}
	} catch (e) {}

	//for (var uuid in session.pcs){ // removing this since it's obsolete at this point.
	//	if (session.pcs[uuid].stats.info && ("version" in session.pcs[uuid].stats.info) &&  (session.pcs[uuid].stats.info.version < 17.2)){
	////		msg.request = "sendroom";
	//		session.sendMsg(msg);
	//		return;
	//	}
	//}
	for (var uuid in session.pcs) {
		if (session.pcs[uuid].scene === scene) {
			session.sendMessage(msg, uuid);
		}
	}
	syncDirectorState(ele);

	if (msg.value) {
		return true;
	} else {
		return false;
	}
}

function syncDirectorState(ele) {
	//if (session.director){ // assumed director, since this is a directEnable sub-function
	var msg = {};
	msg.directorState = getDetailedState(ele.dataset.sid);

	for (var uuid in session.pcs) {
		if (session.pcs[uuid].coDirector) {
			session.sendMessage(msg, uuid);
		}
	}
	for (var i in session.directorList) {
		var uuid = session.directorList[i];
		if (session.rpcs[uuid]) {
			session.sendRequest(msg, uuid);
		}
	}
	
	pokeAPI("details", msg.directorState);
}

function getQuickStats(sid = false) {
	var stats = {};
	try {
		stats.inbound = {};
		stats.outbound = {};
		for (var i in session.rpcs) {
			if (session.rpcs[i].streamID) {
				stats.inbound[session.rpcs[i].streamID] = session.rpcs[i].stats;
			}
		}
		for (var i in session.pcs) {
			stats.outbound[i] = session.pcs[i].stats;
		}
	} catch (e) {}
	if (sid) {
		if (sid in stats.inbound) {
			return stats.inbound[sid];
		} else {
			return null;
		}
	}
	return stats;
}

function getDetailedState(sid = false) {
	var streamList = {};
	var guestFeeds = document.getElementById("guestFeeds");

	for (var UUID in session.rpcs) {
		if (session.rpcs[UUID].streamID) {
			if (sid && sid !== session.rpcs[UUID].streamID) {
				continue;
			}
			let item = {};
			item.streamID = session.rpcs[UUID].streamID;
			item.label = session.rpcs[UUID].label;
			item.group = session.rpcs[UUID].group;

			try {
				item.layout = session.rpcs[UUID].layout;
				
				if (session.director && session.slotmode) {
					item.slot = getSlotState(UUID);
				} else if (session.currentSlots) {
					item.slot = Object.keys(session.currentSlots).find(key => session.currentSlots[key] === session.rpcs[UUID].streamID) || false;
				}
				
				if (item.slot) {
					item.slot = parseInt(item.slot);
				}
				
				if (session.director) {
					let featured = query("[data--u-u-i-d='" + UUID + "'][data-action-type='solo-video']");
					if (featured && parseInt(featured.value)) {
						item.featured = true;
					} else {
						item.featured = false;
					}
				} else if (session.infocus && session.infocus === UUID) {
					item.featured = true;
				} else {
					item.featured = false;
				}
			} catch (e) {
				errorlog(e);
			}

			item.iframeSrc = session.rpcs[UUID].iframeSrc;
			item.localStream = false;
			item.muted = session.rpcs[UUID].remoteMuteState;
			item.videoMuted = session.rpcs[UUID].videoMuted;
			try {
				item.activeSpeaker = session.rpcs[UUID].activelySpeaking;
				item.defaultSpeaker = session.rpcs[UUID].defaultSpeaker;
			} catch (e) {
				errorlog(e);
			}

			item.videoVisible = session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.checkVisibility();
			if (session.rpcs[UUID].videoElement) {
				item.videoVolume = session.rpcs[UUID].videoElement.volume;
			}
			item.iframeVisible = session.rpcs[UUID].iframeVisible && session.rpcs[UUID].iframeVisible.checkVisibility();

			if (session.directorList.indexOf(UUID) >= 0) {
				item.director = true;
			} else {
				item.director = false;
			}
			try {
				if (session.director) {
					if (guestFeeds) {
						var lock = parseInt(document.getElementById("position_" + UUID).dataset.locked);
						if (lock) {
							item.position = lock; // probably should make a universal function to do this, for all lock requesting
						} else {
							var child = document.getElementById("container_" + UUID);
							if (child) {
								var parent = child.parentNode;
								if (parent.id == "guestFeeds") {
									item.position = Array.prototype.indexOf.call(parent.children, child) + 1;
								}
							}
						}
					}
					var scenes = getById("container_" + UUID).querySelectorAll('[data-action-type="addToScene"][data-scene][data--u-u-i-d="' + UUID + '"]');
					var sceneState = {};
					for (var i = 0; i < scenes.length; i++) {
						if (scenes[i].value == 1) {
							sceneState[scenes[i].dataset.scene] = true;
						} else {
							sceneState[scenes[i].dataset.scene] = false;
						}
					}
					item.scenes = sceneState;
					var others = getById("container_" + UUID).querySelectorAll('[data-action-type][data--u-u-i-d="' + UUID + '"]');
					var otherState = {};
					for (var i = 0; i < others.length; i++) {
						if ("scene" in others[i].dataset) {
							continue;
						} else if ("toggle-group" == others[i].dataset.actionType) {
							continue;
						} else if ("value" in others[i]) {
							if (others[i].value !== "") {
								otherState[others[i].dataset.actionType] = others[i].value;
							}
						} else {
							try {
								if (others[i].querySelector(".altpress")) {
									otherState[others[i].dataset.actionType] = "alt";
								} else if (others[i].querySelector(".pressed")) {
									otherState[others[i].dataset.actionType] = true;
								} else if (others[i].dataset.actionType == "soloChat") {
									otherState[others[i].dataset.actionType] = false;
								}
							} catch (e) {
								errorlog(e);
							}
						}
					}
					item.others = otherState;
				}
			} catch (e) {}

			streamList[session.rpcs[UUID].streamID] = item;
		}
	}

	if (sid && sid !== session.streamID) {
		return streamList;
	}

	streamList[session.streamID] = {};

	try {
		if (session.director) {
			var sceneState = {};
			var scenes = getById("container_director").querySelectorAll('[data-action-type="addToScene"][data-scene]');
			for (var i = 0; i < scenes.length; i++) {
				if (scenes[i].value == 1) {
					sceneState[scenes[i].dataset.scene] = true;
				} else {
					sceneState[scenes[i].dataset.scene] = false;
				}
			}
			streamList[session.streamID].scenes = sceneState;
		}
	} catch (e) {}

	if (session.director) {
		let featured = document.querySelector("#highlightDirector[data-action-type='solo-video'], #container_director [data-action-type='solo-video']");
		if (featured && parseInt(featured.value)) {
			streamList[session.streamID].featured = true;
		} else {
			streamList[session.streamID].featured = false;
		}
	} else if (session.infocus && session.infocus === true) {
		streamList[session.streamID].featured = true;
	} else {
		streamList[session.streamID].featured = false;
	}

	streamList[session.streamID].label = session.label;
	streamList[session.streamID].meta = session.meta;
	streamList[session.streamID].group = session.group;
	streamList[session.streamID].groupView = session.groupView;
	streamList[session.streamID].scene = session.scene;
	streamList[session.streamID].streamID = session.streamID;
	streamList[session.streamID].iframeSrc = session.iframeSrc;
	streamList[session.streamID].director = session.directorState; //session.director is what you want to be; session.directorState is what you are
	streamList[session.streamID].localstream = true; // deprecated.
	streamList[session.streamID].localStream = true;
	streamList[session.streamID].seeding = session.seeding;
	streamList[session.streamID].muted = session.muted;
	streamList[session.streamID].videoMuted = session.videoMuted;
	streamList[session.streamID].videoVisible = session.videoElement && session.videoElement.checkVisibility();
	streamList[session.streamID].speakerMuted = session.speakerMuted;
	streamList[session.streamID].position = null;
	streamList[session.streamID].meshcast = session.meshcast;
	streamList[session.streamID].layout = session.layout;

	try {
		if (session.streamID && session.slotmode) {
			// Properly check for director's slots by looking directly at currentSlots
			let directorSlot = false;
			
			// Look for the director's main stream in currentSlots
			Object.entries(session.currentSlots).forEach(([slot, sid]) => {
				if (sid === session.streamID) {
					directorSlot = parseInt(slot);
				}
			});
			
			// If the director has a slot assigned, use it
			if (directorSlot) {
				streamList[session.streamID].slot = directorSlot;
			} else {
				// No slot found for director
				streamList[session.streamID].slot = false;
			}
		}
	} catch (e) {
		errorlog(e);
	}

	if (session.info && session.info.out) {
		streamList[session.streamID].outbound = session.info.out;
	}

	if (session.showDirector && session.director) {
		var child = document.getElementById("container_director");
		if (child) {
			var parent = child.parentNode;
			if (parent.id == "guestFeeds") {
				streamList[session.streamID].position = Array.prototype.indexOf.call(parent.children, child) + 1;
			}
		}
	}

	if (session.notifyScreenShare) {
		streamList[session.streamID].screenSharing = session.screenShareState;
	} else {
		streamList[session.streamID].screenSharing = false;
	}

	if (session.streamSrc) {
		streamList[session.streamID].audioTrack = session.streamSrc.getAudioTracks().length !== 0;
		streamList[session.streamID].videoTrack = session.streamSrc.getVideoTracks().length !== 0;
	} else {
		streamList[session.streamID].audioTrack = false;
		streamList[session.streamID].videoTrack = false;
	}

	return streamList;
}

function getGuestList() {
	var guestFeeds = document.getElementById("guestFeeds");
	if (!guestFeeds) {
		return {};
	}
	var streamList = {};
	for (var i = 0; i < guestFeeds.children.length; i++) {
		try {
			if (session.rpcs[guestFeeds.children[i].dataset.UUID]) {
				streamList[i + 1 + ""] = { streamID: session.rpcs[guestFeeds.children[i].dataset.UUID].streamID, label: session.rpcs[guestFeeds.children[i].dataset.UUID].label || "" };
			} else if (guestFeeds.children[i].id == "container_director") {
				streamList[i + 1 + ""] = { streamID: session.streamID, label: session.label || "" };
			} else if (guestFeeds.children[i].id == "container_screen_director") {
				streamList[i + 1 + ""] = { streamID: session.streamID + ":s", label: session.screenShareLabel || "" };
			}
		} catch (e) {
			errorlog(e);
		}
	}

	return streamList;
}

function syncOtherState(sid) {
	if (!session.syncState) {
		return;
	}
	if (!session.syncState[sid]) {
		return;
	}

	/* if (session.rpcs[ele.dataset.UUID].directorMutedState==1){
		pokeIframeAPI("director-mute-state", true, ele.dataset.UUID);
		pokeAPI("directorMuted", true, session.rpcs[ele.dataset.UUID].streamID);
	} else {
		pokeIframeAPI("director-mute-state", false, ele.dataset.UUID);
		pokeAPI("directorMuted", false, session.rpcs[ele.dataset.UUID].streamID);
	} */

	var others = session.syncState[sid].others;

	log(others);

	for (var other in others) {
		if (other == "toggle-group") {
			continue;
		}
		var ele = document.querySelector('[data-sid="' + sid + '"][data-action-type="' + other + '"]');
		if (ele) {
			if (others[other]) {
				if (!("value" in ele)) {
					errorlog("NO DEFAULT VALUE IN SPECIFIED ELEMENT; guessing default: " + other);
					ele.value = 0;
				}
				var changed = true;
				if (ele.value == others[other]) {
					changed = false;
				}
				if (other == "mute-guest") {
					if (changed) {
						remoteMute(ele, false, true);
					}
				} else if (other == "hide-guest") {
					if (changed) {
						remoteHideVideo(ele, true, true);
					}
				} else if (other == "mute-video-guest") {
					if (changed) {
						remoteMuteVideo(ele, true, true);
					}
				} else {
					ele.value = others[other];

					if (ele.nodeName.toLowerCase() == "input") {
						ele.value = parseInt(others[other]);
					} else if (parseInt(others[other])) {
						ele.classList.add("pressed");
						ele.ariaPressed = "true";
					} else {
						ele.classList.remove("pressed");
						ele.ariaPressed = "false";
					}
				}
			}
		}
	}

	var UUID = document.querySelector('[data-sid="' + sid + '"][data--u-u-i-d');
	if (UUID && UUID.dataset.UUID) {
		UUID = UUID.dataset.UUID;
		if (session.syncState[sid].group && session.rpcs[UUID]) {
			session.rpcs[UUID].group = session.syncState[sid].group;
			syncGroup(session.rpcs[UUID].group, UUID);
		}
	}
}

function htmlToElement(html) {
	var template = document.createElement("template");
	html = html.trim(); // Never return a text node of whitespace as the result
	template.innerHTML = html;
	return template.content.firstChild;
}

function syncGroup(groups, UUID) {
	if (!groups || typeof groups !== "object") {
		errorlog("Group isn't an object");
		return;
	}
	groups.forEach(group => {
		var ele = getById("container_" + UUID).querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]');
		if (!ele) {
			var newGroup = htmlToElement('<button style="margin: 0 5px 10px 5px;" class="pressed" data-sid="' + session.rpcs[UUID].streamID + '" data--u-u-i-d="' + UUID + '" data-action-type="toggle-group" data-group="' + group + '"   title="Add to Group: ' + group + '" onclick="changeGroup(this, event);"><span ><i class="las la-users" style="color:#060"></i>' + group + "</span></button>");

			var added = false;
			getById("container_" + UUID)
				.querySelectorAll(".customGroup>[data-group]")
				.forEach(ele => {
					log(ele);
					if (!added && ele.dataset.group > group + "") {
						ele.parentNode.insertBefore(newGroup, ele);
						added = true;
					}
				});
			if (!added) {
				var newGroupCon = getById("container_" + UUID).querySelector(".customGroup");
				if (!newGroupCon) {
					newGroupCon = document.createElement("div");
					newGroupCon.classList.add("customGroup");
					getById("container_" + UUID).appendChild(newGroupCon);
				}
				newGroupCon.appendChild(newGroup);
			}
		}
	});

	var elements = document.querySelectorAll('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group]');
	if (elements.length) {
		for (var i = 0; i < elements.length; i++) {
			if (session.rpcs[UUID].group.includes(elements[i].dataset.group)) {
				elements[i].classList.add("pressed");
				elements[i].ariaPressed = "true";
			} else {
				elements[i].classList.remove("pressed");
				elements[i].ariaPressed = "false";
			}
		}
		log("synced group");
	} else {
		log("not syncing group buttons; don't exist");
	}
}

function syncSceneState(sid) {
	if (!session.syncState) {
		return;
	}
	if (!session.syncState[sid]) {
		return;
	}
	var scenes = session.syncState[sid].scenes || [];
	for (var scene in scenes) {
		try {
			var ele = document.querySelector('[data-sid="' + sid + '"][data-action-type="addToScene"][data-scene="' + scene + '"]');
			if (ele) {
				if (scenes[scene]) {
					ele.value = 1;
					ele.classList.add("pressed");
					ele.ariaPressed = "true";
					getById("container_" + ele.dataset.UUID).classList.add("containerGreen");
					if (ele.children[1]) {
						ele.children[1].innerHTML = "Remove";
					}
				} else {
					ele.value = 0;
					ele.classList.remove("pressed");
					ele.ariaPressed = "false";
					if (ele.children[1]) {
						ele.children[1].innerHTML = "Add to Scene " + scene;
					}
				}
			}
		} catch (e) {}
	}
}

function issueLayout(scene = false, UUID = false) {
	// A directing room only is controlled by the Director, with the exception of MUTE.
	log("issueLayout() called");
	
	var msg = {};
	msg.layout = session.layout;
	msg.layout_array = session.layout_array;
	
	//try {
	//	pokeIframeAPI("layout", {layout:layout, scene:scene});
	//} catch(e){}

	/* session.layout = {
		"stevetestA": {
			x:0,
			y:0,
			w:40,
			h:40,
			z:0,
			c:false
			
		},
		"stevetestB": {
			x:50,
			y:50,
			w:40,
			h:40,
			z:1,
			c:true
		}
	}; */

	if (UUID) {
		if (session.pcs[UUID] && scene !== false && session.pcs[UUID].scene === scene + "" && !session.pcs[UUID].solo && session.pcs[UUID].layout) {
			// scene specified
			session.sendMessage(msg, UUID);
		} else if (session.pcs[UUID] && session.pcs[UUID].layout && !session.pcs[UUID].solo) {
			// no scene targetted
			session.sendMessage(msg, UUID);
			log("broadcast");
		}
	} else {
		for (var uuid in session.pcs) {
			if (scene !== false && session.pcs[uuid].scene === scene + "" && !session.pcs[uuid].solo && session.pcs[uuid].layout) {
				session.sendMessage(msg, uuid);
			} else if (session.pcs[uuid].layout && !session.pcs[uuid].solo) {
				session.sendMessage(msg, uuid);
				log("broadcast");
			}
		}
	}
}

async function issueLayoutOBS(data) {
	// A directing room only is controlled by the Director, with the exception of MUTE.

	var layout = data.layout || false;
	var scene = data.scene || false;
	var UUID = data.UUID || false;
	var obsCommand = data.obsCommand || false;

	log("issueLayoutOBS() called");
	var msg = {};
	msg.layout = layout;
	msg.obsCommand = obsCommand;

	if (data.remote) {
		msg.remote = data.remote;
	} else {
		msg.remote = session.remote || true;
	}
	msg = await session.encodeRemote(msg);

	if (UUID) {
		try {
			log("CONTROL STATE" + session.pcs[UUID].obsState.details.controlLevel);
		} catch (e) {}

		if (session.pcs[UUID] && scene !== false && session.pcs[UUID].scene === scene + "") {
			if (!session.pcs[UUID].solo) {
				session.sendMessage(msg, UUID);
			}
		} else if (session.pcs[UUID] && session.pcs[UUID].layout) {
			session.sendMessage(msg, UUID);
			log("broadcast");
		}
	} else {
		for (var uuid in session.pcs) {
			try {
				log("CONTROL STATE" + session.pcs[UUID].obsState.details.controlLevel);
			} catch (e) {}

			if (scene !== false && session.pcs[uuid].scene === scene + "") {
				if (!session.pcs[uuid].solo) {
					session.sendMessage(msg, uuid);
				}
			} else if (session.pcs[uuid].layout) {
				session.sendMessage(msg, uuid);
				log("broadcast");
			}
		}
	}
}

var previousURL = "";
var stillNeedURL = true;
var reloadCancelled = false;
var armedReload = false;

async function directPageReload(ele, event) {
	log("URL Page reload");
	if (event === false) {
		if (previousURL === null) {
			// user cancelled in previous callback
			ele.innerHTML = '<i class="las la-sync"></i> <span data-translate="change-url">change URL</span>';
			miniTranslate(ele);
			ele.classList.remove("armed"); // ele.style.backgroundColor = null;
			return;
		}
		if (reloadCancelled === true) {
			ele.innerHTML = '<i class="las la-sync"></i> <span data-translate="change-url">change URL</span>';
			miniTranslate(ele);
			ele.classList.remove("armed");
			//ele.style.backgroundColor = null;
			return;
		}
		reloadURL = previousURL;
	} else if (event.ctrlKey || event.metaKey) {
		ele.innerHTML = '<i class="las la-check"></i> <span data-translate="button-armed">armed</span>';
		miniTranslate(ele);
		ele.classList.add("armed");
		//ele.style.backgroundColor = "#BF3F3F";
		reloadCancelled = false;
		armedReload = true;
		Callbacks.push([directPageReload, ele, stillNeedURL]);
		stillNeedURL = false;
		log("URL update queued");
		return;
	} else if (armedReload) {
		reloadURL = previousURL;
	} else {
		window.focus();
		var reloadURL = await promptAlt(getTranslation("transfer-guest-to-url"), false, false, previousURL);
		stillNeedURL = true;
		if (reloadURL === null) {
			// user cancelled
			ele.innerHTML = '<i class="las la-sync"></i> <span data-translate="change-url">change URL</span>';
			miniTranslate(ele);
			ele.classList.remove("armed");
			//ele.style.backgroundColor = null;
			reloadCancelled = true;
			return;
		}
		try {
			previousURL = reloadURL;
		} catch (e) {}
	}
	ele.innerHTML = '<i class="las la-sync"></i> <span data-translate="change-url">change URL</span>';
	miniTranslate(ele);
	ele.classList.remove("armed"); //ele.style.backgroundColor = null;

	if (reloadURL) {
		previousURL = reloadURL;

		var msg = {};
		msg.changeURL = reloadURL;
		if (ele.dataset.UUID in session.rpcs) {
			session.rpcs[ele.dataset.UUID].receiveChannel.send(JSON.stringify(msg));
		}
	}
}

async function directTimer(ele, event = false, manualSetTime = false) {
	// A directing room only is controlled by the Director, with the exception of MUTE.
	log("directTimer");
	var msg = {};
	ele.classList.remove("blue");
	ele.classList.remove("red");
	if (!event || !(event.ctrlKey || event.metaKey)) {
		if (ele.value == 0 || ele.value == 2) {
			if (manualSetTime !== false) {
				var getTime = parseFloat(manualSetTime) || 0;
			} else {
				var getTime = await promptAlt("Time to set count down timer", false, false, parseInt(getById("overlayClockContainer").dataset.initial), true);
			}
			if (getTime === null) {
				return;
			}
			getById("overlayClockContainer").dataset.initial = parseInt(getTime);
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			ele.classList.remove("red");
			msg.setClock = getTime;
			msg.showClock = true;
			msg.startClock = true;

			ele.innerHTML = '<i class="las la-clock"></i><span data-translate="create-timer"> Remove Timer</span>';
		} else if (ele.value == 3) {
			ele.value = 1;
			msg.resumeClock = true;
			ele.classList.add("red");
		} else {
			ele.value = 2;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			msg.stopClock = true;
			msg.hideClock = true;
			ele.innerHTML = '<i class="las la-clock"></i><span data-translate="create-timer"> Create Timer</span>';
		}
		//miniTranslate(ele);
	} else if (event.ctrlKey || event.metaKey) {
		if (ele.value == 1) {
			ele.value = 3;
			msg.pauseClock = true;
			ele.classList.add("blue");
		} else if (ele.value == 3) {
			ele.value = 1;
			msg.resumeClock = true;
			ele.classList.add("red");
		}
	}
	
	if (!session.director){
		return;
	}

	if (ele.dataset.UUID) {
		if (session.sendRequest(msg, ele.dataset.UUID)) {
			return true;
		}
	} else {
		if (session.sendRequest(msg)) {
			return true;
		}
	}
	return false;
}
function formatTime24h(date, clock24 = true) {
  let hours = date.getHours();
  let minutes = date.getMinutes();
  
  // Ensure hours are always 0-23
  hours = hours % 24;
  
  // Pad single digit hours and minutes with leading zeros
  hours = hours.toString().padStart(2, '0');
  minutes = minutes.toString().padStart(2, '0');
  
  if (clock24) {
    // 24-hour format
    return `${hours}:${minutes}`;
  } else {
    // 12-hour format
    let period = hours >= 12 ? 'PM' : 'AM';
    hours = hours % 12 || 12; // Convert 0 to 12 for midnight
    return `${hours}:${minutes} ${period}`;
  }
}

function toggleClock(clock24 = session.clock24) {
	if (session.showTime === false) {
		return;
	}
	if (session.showTime) {
		clearInterval(session.showTime);
		session.showTime = null;
		var clock = getById("overlayClock2");
		clock.ctx = null;
		clock.canvas = null;

		if (document.pictureInPictureElement && clock.video) {
			if (document.pictureInPictureElement == clock.video) {
				document.exitPictureInPicture();
				pokeIframeAPI("picture-in-picture", false);
			}
			clock.video.remove;
		}
		clock.video = null;
		clock.innerHTML = "";
		getById("overlayClockContainer2").classList.add("hidden");
	} else {
		var time = new Date();

		var clock = getById("overlayClock2");
		if (clock.ctx) {
			clock.ctx.beginPath();
			clock.ctx.rect(0, 0, 230, 40);
			clock.ctx.fillStyle = "#000";
			clock.ctx.fill();
			clock.ctx.fillStyle = "#FFF";
			clock.ctx.font = "50px monospace";
			clock.ctx.textAlign = "center";
			clock.ctx.fillText(formatTime24h(time,clock24), 115, 37);
		} else {
			clock.innerHTML = formatTime24h(time,clock24);
		}

		session.showTime = setInterval(function () {
			var time = new Date();

			var clock = getById("overlayClock2");
			if (clock.ctx) {
				clock.ctx.beginPath();
				clock.ctx.rect(0, 0, 230, 40);
				clock.ctx.fillStyle = "#000";
				clock.ctx.fill();
				clock.ctx.fillStyle = "#FFF";
				clock.ctx.font = "50px monospace";
				clock.ctx.textAlign = "center";
				clock.ctx.fillText(formatTime24h(time,clock24), 115, 37);
			} else {
				getById("overlayClock2").innerHTML = formatTime24h(time,clock24);
			}
		}, 2000);
		getById("overlayClockContainer2").classList.remove("hidden");
	}
	return;
}

async function directRoomClock(ele, event = false) {
	if (ele.active) {
		ele.active = false;
		session.showRoomTime = false;
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
	} else {
		ele.active = true;
		session.showRoomTime = true;
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
	}

	if (session.showTime !== false) {
		if (ele.active && !session.showTime) {
			toggleClock();
		} else if (!ele.active && session.showTime) {
			toggleClock();
		}
	}
	var msg = {};
	if (session.clock24 !== null) {
		msg.clock24 = session.clock24;
	}
	msg.showTime = session.showRoomTime;
	session.sendRequest(msg);
}
async function directRoomTimer(ele, event = false, preSetTime = false) {
	// A directing room only is controlled by the Director, with the exception of MUTE.
	log("directGlobalRoomTimer");
	var msg = {};
	ele.classList.remove("blue");
	ele.classList.remove("red");

	getById("overlayClockContainer").style.fontSize = "50px";

	if (!event || !(event.ctrlKey || event.metaKey)) {
		if (ele.value == 0 || ele.value == 2) {
			if (preSetTime !== false) {
				var getTime = preSetTime;
			} else {
				var getTime = await promptAlt("Time to set count down timer", false, false, parseInt(getById("overlayClockContainer").dataset.initial), true);
			}
			if (getTime === null) {
				return;
			}
			getTime = parseInt(getTime);
			getById("overlayClockContainer").dataset.initial = getTime;
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			ele.classList.remove("red");

			session.roomTimer = Date.now() / 1000 + getTime;

			msg.setClock = getTime;
			setClock(getTime);
			msg.showClock = true;
			showClock();
			msg.startClock = true;
			startClock();
			ele.innerHTML = '<i class="las la-clock"></i><span data-translate="remove-timer"> Remove Timer</span>';
		} else if (ele.value == 3) {
			ele.value = 1;
			msg.resumeClock = true;
			resumeClock(); // this needed to be removed, right?
			if (!session.roomTimer) {
				session.roomTimer = false;
			} else if (session.roomTimer > 0) {
				session.roomTimer = false;
			} else {
				session.roomTimer = Date.now() / 1000 - session.roomTimer;
			}
			ele.innerHTML = '<i class="las la-clock"></i><span data-translate="remove-timer"> Remove Timer</span>';
			ele.classList.add("red");
		} else {
			ele.value = 2;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			session.roomTimer = false;
			msg.stopClock = true;
			stopClock();
			msg.hideClock = true;
			hideClock();
			ele.innerHTML = '<i class="las la-clock"></i><span data-translate="create-timer"> Create Timer</span>';
		}
		//miniTranslate(ele);
	} else if (event.ctrlKey || event.metaKey) {
		if (ele.value == 1) {
			ele.value = 3;
			msg.pauseClock = true;
			pauseClock();
			if (!session.roomTimer) {
				session.roomTimer = false;
			} else if (session.roomTimer < Date.now() / 1000) {
				session.roomTimer = false;
			} else {
				session.roomTimer = Date.now() / 1000 - session.roomTimer;
			}
			ele.innerHTML = '<i class="las la-clock"></i><span data-translate="resume-timer"> Resume Timer</span>';
			ele.classList.add("blue");
		} else if (ele.value == 3) {
			ele.value = 1;
			msg.resumeClock = true;
			resumeClock();
			if (!session.roomTimer) {
				session.roomTimer = false;
			} else if (session.roomTimer > 0) {
				session.roomTimer = false;
			} else {
				session.roomTimer = Date.now() / 1000 - session.roomTimer;
			}
			ele.innerHTML = '<i class="las la-clock"></i><span data-translate="remove-timer"> Remove Timer</span>';
			ele.classList.add("red");
		}
	}
	if (ele.dataset.UUID) {
		session.sendRequest(msg, ele.dataset.UUID);
	} else {
		session.sendRequest(msg);
	}
}

function updateRemoteTimerButton(UUID, currentTime) {
	var elements = document.querySelectorAll('[data-action-type="create-timer"][data--u-u-i-d="' + UUID + '"]');
	if (elements[0]) {
		if (elements[0].value != 2) {
			var time = parseInt(currentTime) || 0;
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
			elements[0].value = 1;
			if (time < 0) {
				time = time * -1;
				var minutes = Math.floor(time / 60);
				var seconds = time - minutes * 60;
				elements[0].classList.add("red");
				elements[0].innerHTML = '<i class="las la-clock"></i> -' + minutes + "m : " + zpadTime(seconds) + "s";
			} else {
				var minutes = Math.floor(time / 60);
				var seconds = time - minutes * 60;
				elements[0].classList.remove("red");
				elements[0].innerHTML = '<i class="las la-clock"></i> ' + minutes + "m : " + zpadTime(seconds) + "s";
			}
		} else {
			elements[0].classList.remove("pressed");
			elements[0].ariaPressed = "false";
			elements[0].classList.remove("red");
			elements[0].innerHTML = '<i class="las la-clock"></i><span data-translate="create-timer"> Create Timer</span>';
		}
	}
}

function directMute(ele, event = false) {
	// A directing room only is controlled by the Director, with the exception of MUTE.
	log("mute 2");

	if (!event || !(event.ctrlKey || event.metaKey)) {
		if (ele.value == 1) {
			ele.value = 0;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			miniTranslate(ele, "mute-scene");
		} else {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			miniTranslate(ele, "unmute");
		}
	}
	var msg = {};
	msg.scene = true;
	msg.action = "mute";
	msg.value = ele.value;
	msg.target = ele.dataset.sid;

	log(msg);
	log("ele:");
	log(ele);
	//for (var uuid in session.pcs){ // obsolete at this point; v22
	//	if (session.pcs[uuid].stats.info && ("version" in session.pcs[uuid].stats.info) && (session.pcs[uuid].stats.info.version < 17.2)){
	//		msg.request = "sendroom";
	//		session.sendMsg(msg);
	//		return;
	//	}
	//}

	for (var uuid in session.pcs) {
		if (session.pcs[uuid].scene !== false) {
			// send to all scenes (but scene = 0)
			session.sendMessage(msg, uuid);
		}
	}

	syncDirectorState(ele);

	if (msg.value) {
		return true;
	} else {
		return false;
	}
}

function requestFileUpload(ele) {
    ele.classList.add("pressed");
    ele.ariaPressed = "true";
    ele.disabled = true;
    ele.innerHTML = '<i class="las la-file-upload"></i><span data-translate="request-upload"> Requesting..</span>';
    setTimeout(
        function (ele) {
            try {
                ele.innerHTML = '<i class="las la-file-upload"></i><span data-translate="request-upload"> Request File</span>';
                ele.classList.remove("pressed");
                ele.ariaPressed = "false";
                ele.disabled = false;
            } catch (e) {}
        },
        15000,
        ele
    );
    var msg = {
        requestUpload: true,
        UUID: ele.dataset.UUID
    };
    session.sendRequest(msg, ele.dataset.UUID);
}

function remoteSpeakerMute(ele, event = false) {
	log("speaker mute");
	if (!event || !(event.ctrlKey || event.metaKey)) {
		if (ele.value == 1) {
			ele.value = 0;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			ele.innerHTML = '<i class="las la-volume-off"></i> <span data-translate="toggle-remote-speaker">Deafen</span>';
		} else {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			ele.innerHTML = '<i class="las la-volume-off"></i> <span data-translate="undeafen">Undeafen</span>';
		}
		miniTranslate(ele);
	}

	var msg = {};
	if (ele.value == 0) {
		msg.speakerMute = false;
	} else {
		msg.speakerMute = true;
	}
	msg.UUID = ele.dataset.UUID;
	session.sendRequest(msg, ele.dataset.UUID);
	syncDirectorState(ele);

	errorlog(msg);

	return msg.speakerMute;
}

function updateRemoteSpeakerMute(UUID) {
	var ele = document.querySelectorAll('[data-action-type="toggle-remote-speaker"][data--u-u-i-d="' + UUID + '"]');
	if (ele[0]) {
		ele[0].classList.add("pressed");
		ele[0].ariaPressed = "true";
		ele[0].value = 1;
		ele[0].innerHTML = '<i class="las la-volume-off"></i> <span data-translate="undeafen">undeafen</span>';
		miniTranslate(ele[0]);
	}
	return true;
}

function updateRemoteDisplayMute(UUID, blind = true) {
	var ele = document.querySelectorAll('[data-action-type="toggle-remote-display"][data--u-u-i-d="' + UUID + '"]');
	if (ele[0]) {
		if (blind) {
			ele[0].classList.add("pressed");
			ele[0].ariaPressed = "true";
			ele[0].value = 1;
			ele[0].innerHTML = '<i class="las la-eye-slash"></i> <span data-translate="unblind">unblind</span>';
			miniTranslate(ele[0]);
			return true;
		} else {
			ele[0].classList.remove("pressed");
			ele[0].ariaPressed = "false";
			ele[0].value = 0;
			ele[0].innerHTML = '<i class="las la-eye"></i> <span data-translate="blind">blind</span>';
			miniTranslate(ele[0]);
			return false;
		}
	}
	return false;
}

function blindAllGuests(ele, event = false) {
	if (!session.director) {
		if (!session.cleanOutput) {
			warnUser("Only a director can mute other guests");
		}
		return;
	} // only a director can use this button.

	log("blind all display mute");
	if (!event || !(event.ctrlKey || event.metaKey)) {
		if (ele.value == 1) {
			ele.value = 0;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			ele.classList.remove("red");
			ele.innerHTML = '<i class="toggleSize las la-eye"></i>';
		} else {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			ele.classList.add("red");
			ele.innerHTML = '<i class="toggleSize las la-eye-slash"></i>';
		}
	}

	var msg = {};
	if (ele.value == 0) {
		msg.displayMute = false;
		session.directorBlindAllGuests = false;
	} else {
		msg.displayMute = true;
		session.directorBlindAllGuests = true;
	}
	for (var UUID in session.rpcs) {
		// doesn't include scenes, as they don't publiish and this is rpcs
		if (session.directorList.indexOf(UUID) >= 0) {
			continue;
		} // don't try to mute other directors
		try {
			session.sendRequest(msg, UUID);
			updateRemoteDisplayMute(UUID, msg.displayMute);
		} catch (e) {
			errorlog(e);
		}
	}
	syncDirectorState(ele);
	return msg.displayMute;
}

function remoteDisplayMute(ele, event = false) {
	log("display mute");
	if (!event || !(event.ctrlKey || event.metaKey)) {
		if (ele.value == 1) {
			ele.value = 0;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			ele.innerHTML = '<i class="las la-eye-slash"></i> <span data-translate="toggle-remote-display">Blind</span>';
		} else {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			ele.innerHTML = '<i class="las la-eye-slash"></i> <span data-translate="unblind">Unblind</span>';
		}
		miniTranslate(ele);
	}

	var msg = {};
	if (ele.value == 0) {
		msg.displayMute = false;
	} else {
		msg.displayMute = true;
	}
	msg.UUID = ele.dataset.UUID;
	session.sendRequest(msg, ele.dataset.UUID);
	syncDirectorState(ele);
	return msg.displayMute;
}

function remoteLowerhands(UUID) {
	var msg = {};
	msg.lowerhand = true;
	msg.UUID = UUID;
	session.sendRequest(msg, UUID);

	try {
		getById("hands_" + UUID).classList.add("hidden");
		session.rpcs[UUID].remoteRaisedHandElement.classList.add("hidden");
	} catch (e) {}
	return true;
}

function remoteMute(ele, event = false, skipSend = false) {
	log("mute");
	var val = parseInt(ele.value) || 0;
	if (!event || !(event.ctrlKey || event.metaKey)) {
		if (val == 1) {
			ele.value = 0;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			//ele.innerHTML = '<i class="las la-microphone-slash"></i> <span data-translate="mute" >mute</span>';
			miniTranslate(ele, "mute");
			//ele.innerHTML += getTranslation("mute");
		} else {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			//ele.innerHTML = '<i class="las la-microphone-slash"></i> <span data-translate="unmute" >unmute</span>';
			miniTranslate(ele, "unmute");
		}
	}

	try {
		session.rpcs[ele.dataset.UUID].directorMutedState = ele.value;
		var volume = session.rpcs[ele.dataset.UUID].directorVolumeState;
	} catch (e) {
		errorlog(e);
		var volume = 100;
	}

	if (!skipSend) {
		var msg = {};
		if (val == 1) {
			msg.volume = volume;
		} else {
			msg.volume = 0;
		}
		msg.UUID = ele.dataset.UUID;
		session.sendRequest(msg, ele.dataset.UUID);
		syncDirectorState(ele);
		log(msg);
	}

	if (session.rpcs[ele.dataset.UUID].directorMutedState == 1) {
		pokeIframeAPI("director-mute-state", true, ele.dataset.UUID);
		pokeAPI("directorMuted", true, session.rpcs[ele.dataset.UUID].streamID);
	} else {
		pokeIframeAPI("director-mute-state", false, ele.dataset.UUID);
		pokeAPI("directorMuted", false, session.rpcs[ele.dataset.UUID].streamID);
	}

	if (val) {
		return true;
	} else {
		return false;
	}
}

function toggleQualityGear3() {
	toggle(document.getElementById("videoSettings3"), (inline = false));
	if (getById("gear_webcam3").style.display === "inline-block") {
		var videoSelect = document.querySelector("select#videoSource3").options;
		var obscam = false;
		log(videoSelect[videoSelect.selectedIndex].text);
		if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) {
			// OBS Virtualcam
			obscam = true;
		} else if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) {
			// OBS Virtualcam
			obscam = true;
		}

		updateStats(obscam);
	}
}

function remoteHideVideo(ele, event = false, skipSend = false) {
	log("video hide");

	if (!event || event.ctrlKey || event.metaKey) {
		//ele.children[1].innerHTML = getTranslation("armed");
		miniTranslate(ele.children[1], "armed");
		//ele.style.backgroundColor = "#BF3F3F";
		ele.classList.add("armed");
		Callbacks.push([remoteHideVideo, ele, false]);
		log("video queued");
		return;
	} else {
		if (ele.value == 1) {
			ele.value = 0;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			ele.innerHTML = '<i class="las la-user-slash"></i> <span data-translate="hide-guest" >Hide</span>';
		} else {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			ele.innerHTML = '<i class="las la-user-slash"></i> <span data-translate="unhide-guest" >Unhide</span>';
		}
		miniTranslate(ele);
		ele.classList.remove("armed"); //ele.style.backgroundColor = null;
	}

	var msg = {};
	if (ele.value == 0) {
		msg.directVideoMuted = false;
	} else {
		msg.directVideoMuted = true;
	}

	if (!skipSend) {
		for (var i in session.pcs) {
			msg.target = ele.dataset.UUID;

			if (i === msg.target) {
				msg.target = true;
			}
			try {
				session.pcs[i].sendChannel.send(JSON.stringify(msg));
			} catch (e) {}
		}
		syncDirectorState(ele);
	}

	pokeIframeAPI("director-video-hide-state", msg.directVideoMuted, ele.dataset.UUID);
	pokeAPI("directorVideoHide", msg.directVideoMuted, session.rpcs[ele.dataset.UUID].streamID);

	return msg.directVideoMuted;
}

function remoteMuteVideo(ele, event = false, skipSend = false) {
	log("video mute");

	if (!event || event.ctrlKey || event.metaKey) {
		//ele.children[1].innerHTML = getTranslation("armed");
		miniTranslate(ele.children[1], "armed");
		ele.classList.add("armed"); //ele.style.backgroundColor = "#BF3F3F";
		Callbacks.push([remoteMuteVideo, ele, false]);
		log("video queued");
		return;
	} else {
		if (ele.value == 1) {
			ele.value = 0;
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
			ele.innerHTML = '<i class="las la-video-slash"></i> <span data-translate="mute-video-guest" >Video off</span>';
		} else {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			ele.innerHTML = '<i class="las la-video-slash"></i> <span data-translate="unmute-video-guest" >Video on</span>';
		}
		miniTranslate(ele);
		ele.classList.remove("armed");
	}

	var msg = {};
	if (ele.value == 0) {
		msg.remoteVideoMuted = false;
	} else {
		msg.remoteVideoMuted = true;
	}

	if (!skipSend) {
		session.sendRequest(msg, ele.dataset.UUID);
		syncDirectorState(ele);
	}

	pokeIframeAPI("remote-video-mute-state", msg.remoteVideoMuted, ele.dataset.UUID);
	pokeAPI("remoteVideoMuted", msg.remoteVideoMuted, session.rpcs[ele.dataset.UUID].streamID);

	return msg.remoteVideoMuted;
}
function updateDirectorVideoHide(UUID) {
	var ele = document.querySelectorAll('[data-action-type="hide-guest"][data--u-u-i-d="' + UUID + '"]');
	if (ele[0]) {
		ele[0].value = 1;
		ele[0].classList.add("pressed");
		ele[0].ariaPressed = "true";
		ele[0].innerHTML = '<i class="las la-user-slash"></i> <span data-translate="unhide-guest" >Unhide</span>';
		miniTranslate(ele[0]);
	}
	return true;
}
function updateDirectorVideoMute(UUID) {
	var ele = document.querySelectorAll('[data-action-type="mute-video-guest"][data--u-u-i-d="' + UUID + '"]');
	if (ele[0]) {
		ele[0].value = 1;
		ele[0].classList.add("pressed");
		ele[0].ariaPressed = "true";
		ele[0].innerHTML = '<i class="las la-video-slash"></i> <span data-translate="unmute-video-guest" >Video on</span>';
		miniTranslate(ele[0]);
	}
	return true;
}

function directVolume(ele) {
	// NOT USED ANYMORE
	log("volume");
	var msg = {};
	msg.scene = true;
	msg.action = "volume";
	msg.target = ele.dataset.sid; // i want to focus on the STREAM ID, not the UUID...
	msg.value = ele.value;

	//for (var uuid in session.pcs){
	//	if (session.pcs[uuid].stats.info  && ("version" in session.pcs[uuid].stats.info) &&  (session.pcs[uuid].stats.info.version < 17.2)){
	//		msg.request = "sendroom";
	//		session.sendMsg(msg);
	//		return;
	//	}
	//}

	for (var uuid in session.pcs) {
		if (session.pcs[uuid].scene !== false) {
			// send to all scenes (but scene = 0)
			session.sendMessage(msg, uuid);
		}
	}

	syncDirectorState(ele);
	return msg.value;
}

function applyMuteState(UUID) {
	// this is the mute state of PLAYBACK audio; not the microphone or outbound.
	if (!(UUID in session.rpcs)) {
		return "UUID not found";
	}
	
	var muteOutcome = session.rpcs[UUID].mutedState || session.rpcs[UUID].mutedStateMixer || session.rpcs[UUID].mutedStateScene || session.speakerMuted || session.rpcs[UUID].bandwidthMuted;
	
	if (session.pauseInvisible){
		if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.isInvisible) {
			muteOutcome = true;
		}
	}

	if (!muteOutcome && session.noaudio !== false) {
		if (session.noaudio === true) {
			muteOutcome = true;
		} else if (session.noaudio.length) {
			if (("streamID" in session.rpcs[UUID]) && session.rpcs[UUID].streamID && !session.noaudio.includes(session.rpcs[UUID].streamID)) {
				muteOutcome = true;
			}
		} else {
			muteOutcome = true;
		}
	} else if (!muteOutcome && session.excludeaudio){
		if (("streamID" in session.rpcs[UUID]) && session.rpcs[UUID].streamID && session.excludeaudio.includes(session.rpcs[UUID].streamID)) {
			muteOutcome = true;
		}
	}

	if (session.rpcs[UUID].videoElement) {
		if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.usermuted === 1) {
			return "usermuted 1";
		}
		if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.usermuted === 2) {
			return "usermuted 2";
		}
		session.rpcs[UUID].videoElement.muted = muteOutcome;
	}

	// session.scene
	return muteOutcome;
}

function checkMuteState(UUID) {
	// this is the mute state of PLAYBACK audio; not the microphone or outbound.
	if (!(UUID in session.rpcs)) {
		return false;
	}
	if (session.pauseInvisible){
		if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.isInvisible) {
			return true;
		}
	}
	return session.rpcs[UUID].mutedState || session.rpcs[UUID].mutedStateMixer || session.rpcs[UUID].mutedStateScene || session.speakerMuted || session.rpcs[UUID].bandwidthMuted;
}

var volumeLUT = [0, 1, 2, 2.4, 2.7, 3, 3.4, 3.7, 4, 4.5, 4.8, 5, 5.6, 6, 6.4, 6.8, 7, 7.7, 8, 8.6, 9, 9.5, 10, 10.4, 10.9, 11, 12, 12.5, 13, 13.6, 14, 14.7, 15, 15.6, 16, 17, 17.7, 18, 19, 19.7, 20, 21, 21.8, 22, 23, 24, 24.7, 25, 26, 27, 28, 28.5, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 48, 49, 51, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 67, 68, 70, 72, 74, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 100, 102, 104, 107, 109, 112, 114, 117, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 156, 159, 163, 166, 170, 174, 177, 181, 185, 189, 193, 198, 202, 206, 211, 215, 220, 225, 230, 234, 240, 245, 250, 255, 261, 266, 272, 278, 284, 290, 296, 302, 308, 315, 322, 328, 335, 342, 350, 357, 364, 372, 380, 388, 396, 404, 413, 421, 430, 439, 448, 458, 467, 477, 487, 497, 507, 518, 528, 539, 551, 562, 574, 586, 598, 610, 623, 635, 649, 662, 676, 690, 704, 718, 733, 748, 764, 779, 795, 812, 828];

function initAudioButtons(audioGain, UUID) {
	if (audioGain === 0) {
		var ele = document.querySelector('[data-action-type="mute-guest"][data--u-u-i-d="' + UUID + '"]');
		if (ele) {
			ele.value = 1;
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
			miniTranslate(ele.children[1], "unmute");
			session.rpcs[UUID].directorMutedState = 1;
		}
		pokeIframeAPI("director-mute-state", true, UUID);
	} else {
		var ele = document.querySelector('[data-action-type="volume"][data--u-u-i-d="' + UUID + '"]');
		if (ele) {
			if (audioGain == 100) {
				ele.value = audioGain;
			} else {
				audioGain = parseInt(audioGain) || 0;
				ele.value = 200;
				for (var i = 1; i <= 200; i++) {
					if (volumeLUT[i] >= audioGain) {
						ele.value = i;
						break;
					}
				}
			}
			session.rpcs[UUID].directorVolumeState = audioGain;
			remoteVolumeUI(ele);
		}
	}
}
function remoteVolumeUI(ele) {
	var value = ele.value;
	value = volumeLUT[parseInt(value)];

	ele.nextElementSibling.innerHTML = value + "%";
	if (Date.now() - remoteSliderTimeout > 100) {
		remoteSliderTimeout = Date.now();
		remoteVolume(ele);
	}
	//setVolumeColor(ele);
	return value;
}

function remoteVolume(ele) {
	// A directing room only is controlled by the Director, with the exception of MUTE.
	log("volume: " + session.rpcs[ele.dataset.UUID].directorMutedState);
	var msg = {};
	var muted = session.rpcs[ele.dataset.UUID].directorMutedState;

	var value = ele.value;
	value = volumeLUT[parseInt(value)];

	//log(ele);
	if (muted == true) {
		// 1 is a string, not an int, so == and not ===. this happens in a few places :/
		session.rpcs[ele.dataset.UUID].directorVolumeState = value;
	} else {
		session.rpcs[ele.dataset.UUID].directorVolumeState = value;
		msg.volume = value;
		msg.UUID = ele.dataset.UUID;
		session.sendRequest(msg, ele.dataset.UUID);
	}

	//const minLog = Math.log10(0.01); // Log10 of minimum gain
	// const maxLog = Math.log10(10);   // Log10 of maximum gain
	// const normalizedValue = (msg.volume / 75) * (maxLog - minLog) + minLog;
	// const gainValue = Math.pow(10, normalizedValue);
	//console.log(gainValue*100);

	pokeIframeAPI("director-volume-state", value, ele.dataset.UUID);

	syncDirectorState(ele);
	return value;
}

/* function setVolumeColor(ele){
	var vol1 = 200-parseInt(ele.value);
	if (vol1<0){vol1=0};
	ele.style.backgroundColor = "hsl("+vol1+", 100%, 50%)";
} */

function clearDirectorSettings() {
	// make sure to wipe the director's room settings if creating a new room.
	//console.warn("Clearing");
	removeStorage("directorCustomize");
	removeStorage("directorWebsiteShare");
}

function saveDirectorSettings() {
	//console.warn("Saving");
	var settings = {};

	if (getById("customizeLinks").classList.contains("hidden")) {
		settings.customizeLinks = true;
	}

	var customizeLinks1 = getById("customizeLinks1").querySelectorAll("input");
	settings.customizeLinks1 = {};
	for (var i = 0; i < customizeLinks1.length; i++) {
		settings.customizeLinks1[customizeLinks1[i].dataset.param] = customizeLinks1[i].checked;
	}

	var customizeLinks3 = getById("customizeLinks3").querySelectorAll("input");
	settings.customizeLinks3 = {};
	for (var i = 0; i < customizeLinks3.length; i++) {
		settings.customizeLinks3[customizeLinks3[i].dataset.param] = customizeLinks3[i].checked;
	}

	var directorLinks1 = getById("directorLinks1").querySelectorAll("input");
	settings.directorLinks1 = {};
	for (var i = 0; i < directorLinks1.length; i++) {
		settings.directorLinks1[directorLinks1[i].dataset.param] = directorLinks1[i].checked;
	}

	var directorLinks2 = getById("directorLinks2").querySelectorAll("input");
	settings.directorLinks2 = {};
	for (var i = 0; i < directorLinks2.length; i++) {
		settings.directorLinks2[directorLinks2[i].dataset.param] = directorLinks2[i].checked;
	}
	setStorage("directorCustomize", settings);
}

function loadDirectorSettings() {
	//console.warn("LOAD DIRECTOR SETTING");
	var settings = getStorage("directorCustomize");
	if (settings.customizeLinks) {
		try {
			hideDirectorinvites(getById("directorLinksButton"), false);
		} catch (e) {
			errorlog(e);
		}
	}

	if (settings.customizeLinks1) {
		var customizeLinks1 = getById("customizeLinks1");
		Object.keys(settings.customizeLinks1).forEach((key, index) => {
			try {
				if (settings.customizeLinks1[key]) {
					customizeLinks1.querySelector('[data-param="' + key + '"]').checked = settings.customizeLinks1[key];
					customizeLinks1.querySelector('[data-param="' + key + '"]').onchange();
				}
			} catch (e) {
				errorlog(e);
			}
		});
	}

	if (settings.customizeLinks3) {
		var customizeLinks3 = getById("customizeLinks3");
		Object.keys(settings.customizeLinks3).forEach((key, index) => {
			try {
				if (settings.customizeLinks3[key]) {
					customizeLinks3.querySelector('[data-param="' + key + '"]').checked = settings.customizeLinks3[key];
					customizeLinks3.querySelector('[data-param="' + key + '"]').onchange();
				}
			} catch (e) {
				errorlog(e);
			}
		});
	}

	if (settings.directorLinks1) {
		var directorLinks1 = getById("directorLinks1");
		Object.keys(settings.directorLinks1).forEach((key, index) => {
			try {
				if (key in settings.directorLinks1) {
					directorLinks1.querySelector('[data-param="' + key + '"]').checked = settings.directorLinks1[key];
					directorLinks1.querySelector('[data-param="' + key + '"]').onchange();
				}
			} catch (e) {
				errorlog("key :" + key);
				errorlog(e);
			}
		});
	}

	if (settings.directorLinks2) {
		var directorLinks2 = getById("directorLinks2");
		Object.keys(settings.directorLinks2).forEach((key, index) => {
			try {
				if (key in settings.directorLinks2) {
					directorLinks2.querySelector('[data-param="' + key + '"]').checked = settings.directorLinks2[key];
					directorLinks2.querySelector('[data-param="' + key + '"]').onchange();
				}
			} catch (e) {
				errorlog("key :" + key);
				errorlog(e);
			}
		});
	}
}

function sendChat(chatmessage = "hi", UUID = false, overlay = false) {
	// A directing room only is controlled by the Director, with the exception of MUTE.
	log("Chat message");
	var msg = {};
	msg.chat = chatmessage;
	msg.overlay = overlay;
	session.sendPeers(msg, UUID);
	return true;
}

var activatedStream = false;

async function publishScreen() {
	if (activatedStream == true) {
		return;
	}
	activatedStream = true;
	setTimeout(function () {
		activatedStream = false;
	}, 1000);

	formSubmitting = false;

	var quality = 0;

	if (document.getElementById("webcamquality2")) {
		quality = parseInt(document.getElementById("webcamquality2").elements.namedItem("resolution2").value) || 0;
	}

	session.quality_ss = quality;

	if (session.quality !== false) {
		quality = session.quality; // override the user's setting
	}

	if (session.screensharequality !== false) {
		quality = session.screensharequality;
	}

	var video = {};

	if (quality == -1) {
		// unlocked capture resolution
	} else if (quality == -2) {
		video.width = {
			ideal: 3840
		};
		video.height = {
			ideal: 2160
		};
	} else if (quality == -3) {
		video.width = {
			ideal: 2560
		};
		video.height = {
			ideal: 1440
		};
	} else if (quality == 0) {
		video.width = {
			ideal: 1920
		};
		video.height = {
			ideal: 1080
		};
	} else if (quality == 1) {
		video.width = {
			ideal: 1280
		};
		video.height = {
			ideal: 720
		};
	} else if (quality == 2) {
		video.width = {
			ideal: 640
		};
		video.height = {
			ideal: 360
		};
	} else if (quality >= 3) {
		// lowest
		video.width = {
			ideal: 320
		};
		video.height = {
			ideal: 180
		};
	} else {
		video.width = {
			min: 640
		};
		video.height = {
			min: 360
		};
	}

	if (session.width) {
		video.width = {
			ideal: session.width
		};
	}
	if (session.height) {
		video.height = {
			ideal: session.height
		};
	}

	var constraints = {
		audio: {
			echoCancellation: false,
			autoGainControl: false,
			noiseSuppression: false
		},
		video: video
	};

	if (session.noiseSuppression === true) {
		constraints.audio.noiseSuppression = true; // the defaults for screen publishing should be off.
	}
	if (session.autoGainControl === true) {
		constraints.audio.autoGainControl = true; // the defaults for screen publishing should be off.
	}
	if (session.echoCancellation === true) {
		constraints.audio.echoCancellation = true; // the defaults for screen publishing should be off.
	}
	if (session.voiceIsolation === true){
		constraint.audio.voiceIsolation = true;
	}
	try {
		let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
		if (supportedConstraints.cursor) {
			if (session.screensharecursor) {
				constraints.video.cursor = ["always", "motion"];
			} else {
				constraints.video.cursor = "never";
			}
		}
		if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) {
			constraints.audio.suppressLocalAudioPlayback = true;
		}
		//
		if (session.preferCurrentTab) {
			constraints.preferCurrentTab = true;
		}
		if (session.selfBrowserSurface) {
			constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include
		}
		if (session.surfaceSwitching) {
			constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include
		}
		if (session.systemAudio) {
			constraints.systemAudio = session.systemAudio; // exclude or include
		}
		if (session.displaySurface && supportedConstraints.displaySurface) {
			constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser
		}
	} catch (e) {
		warnlog("navigator.mediaDevices.getSupportedConstraints() not supported");
	}

	var overrideFramerate = false;
	if (session.frameRate !== false && session.maxframeRate != false) {
		overrideFramerate = session.frameRate;
		constraints.video.frameRate = {
			ideal: session.maxframeRate,
			max: session.maxframeRate
		};
	} else if (session.frameRate !== false) {
		constraints.video.frameRate = session.frameRate;
	} else if (session.maxframeRate != false) {
		constraints.video.frameRate = {
			ideal: session.maxframeRate,
			max: session.maxframeRate
		};
	} else {
		constraints.video.frameRate = {
			ideal: 60
		};
	}

	var outputSelect = getById("outputSourceScreenshare");

	try {
		session.sink = outputSelect.options[outputSelect.selectedIndex].value; // will probably fail on Safari.
		log("Session Sink: " + session.sink);
		saveSettings();
	} catch (e) {
		warnlog(e);
	}
	
	session.audioDevice = selectedScreenShareAudioDevices;

	return await publishScreen2(constraints, selectedScreenShareAudioDevices, true, overrideFramerate)
		.then(res => {
			if (res == false) {
				return;
			} // no screen selected
			log("streamID is: " + session.streamID);
			if (session.transcript) {
				setTimeout(function () {
					setupClosedCaptions();
				}, 1000);
			}

			if (!session.cleanOutput) {
				getById("mutebutton").classList.remove("hidden");
				getById("mutespeakerbutton").classList.remove("hidden");
				//getById("mutespeakerbutton").className="float";
				getById("chatbutton").className = "float";
				getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though.
				getById("mutevideobutton").className = "float";
				getById("hangupbutton").className = "float";
				if (session.showSettings) {
					getById("settingsbutton").className = "float";
				}
				if (session.raisehands) {
					getById("raisehandbutton").className = "float";
				}
				if (session.pptControls) {
					getById("pptbackbutton").classList.remove("hidden");
					getById("pptnextbutton").classList.remove("hidden");
				}
				if (session.recordLocal !== false) {
					getById("recordLocalbutton").classList.remove("hidden");
				}
				if (session.screensharebutton) {
					getById("screensharebutton").className = "float";
				}
				getById("controlButtons").classList.remove("hidden");
				//getById("helpbutton").style.display = "inherit";
				//getById("reportbutton").style.display = "";
			} else if (session.cleanish && session.recordLocal !== false) {
				getById("recordLocalbutton").classList.remove("hidden");
				getById("mutebutton").classList.add("hidden");
				getById("mutespeakerbutton").classList.add("hidden");
				getById("chatbutton").classList.add("hidden");
				getById("mutevideobutton").classList.add("hidden");
				getById("hangupbutton").classList.add("hidden");
				getById("hangupbutton2").classList.add("hidden");
				getById("controlButtons").classList.remove("hidden");
				getById("settingsbutton").classList.add("hidden");
				getById("screenshare2button").classList.add("hidden");
				getById("screensharebutton").classList.add("hidden");
				getById("screenshare3button").classList.add("hidden");
				getById("queuebutton").classList.add("hidden");
			} else {
				getById("controlButtons").classList.add("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.screensharebutton === true) {
				getById("controlButtons").classList.remove("hidden");
				getById("screensharebutton").className = "float";
			}

			if (session.hangupbutton === true) {
				getById("controlButtons").classList.remove("hidden");
				getById("hangupbutton").className = "float";
			}

			getById("head1").className = "hidden";
			getById("head2").className = "hidden";

			return res;
		})
		.catch(() => {});
}

function getWidth() {
	return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth, document.documentElement.clientWidth);
}

function getHeight() {
	return Math.max(document.documentElement.clientHeight);
}

function updateForceRotate(skipLastBit = false) {
	var capabilities = { facingMode: "unknown" };
	var FirefoxSucks = false;

	if (session.orientation) {
		try {
			var track = false;
			if (session.streamSrc) {
				var tracks = session.streamSrc.getVideoTracks();
				if (tracks.length) {
					track = tracks[0];
				}
			}
			if (!track) {
				return;
			}

			const settings = track.getSettings();
			session.currentCameraConstraints = settings;

			if (screen && screen.orientation && screen.orientation.type) {
				if (!screen.orientation.type.includes("portrait")) {
					if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
						session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
					}
				}
			} else if (!window.matchMedia("(orientation: portrait)").matches) {
				if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
					session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
				}
			}

			session.forceRotate = 0;

			if (track.getCapabilities) {
				capabilities = track.getCapabilities(); // firefox sucks?

				if ("width" in settings) {
					if ("height" in settings) {
						if (settings.width < settings.height) {
							if (session.orientation == "landscape") {
								if (capabilities) {
									if (capabilities.facingMode == "environment") {
										session.forceRotate = 270;
									} else {
										session.forceRotate = 90;
									}
								}
							} else {
								session.forceRotate = 0;
							}
						} else if (settings.width > settings.height) {
							if (session.orientation == "portrait") {
								if (capabilities) {
									if (capabilities.facingMode == "environment") {
										session.forceRotate = 90;
									} else {
										session.forceRotate = 270;
									}
								}
							} else {
								session.forceRotate = 0;
							}
						} else {
							session.forceRotate = 0;
						}
					} else {
						return;
					}
				}
			} else if (Firefox && session.mobile) {
				// firefox sucks...
				try {
					var vs1 = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3");
					if (vs1) {
						vs1 = vs1.options[vs1.selectedIndex].textContent;
						if (vs1.includes(" back")) {
							capabilities = { facingMode: "environment" };
							FirefoxSucks = 2;
						} else if (vs1.includes(" front")) {
							capabilities = { facingMode: "user" };
							FirefoxSucks = 1;
						}
					}
					getById("videosource").style.transform = "";
					if (session.orientation == "landscape") {
						if (FirefoxSucks === 1) {
							session.forceRotate = 90;
							// video needs to be flipped
							getById("videosource").style.transform = "rotate(90deg)";
						} else if (FirefoxSucks === 2) {
							getById("videosource").style.transform = "rotate(270deg)";
							session.forceRotate = 270;
						}
					}
				} catch (e) {
					errorlog(e);
				}
			} else {
				return;
			}

			var msg = {};
			msg.rotate_video = 0;
			if (session.forceRotate !== false) {
				if (session.rotate) {
					msg.rotate_video = session.forceRotate + parseInt(session.rotate);
				} else {
					msg.rotate_video = session.forceRotate;
				}
			} else {
				msg.rotate_video = parseInt(session.rotate) || 0;
			}

			if (msg.rotate_video && msg.rotate_video >= 360) {
				msg.rotate_video -= 360;
			}

			for (var UUID in session.pcs) {
				try {
					if (session.pcs[UUID].rotation != msg.rotate_video) {
						//  0 == false will skip I think
						session.pcs[UUID].rotation = msg.rotate_video;
						session.sendMessage(msg, UUID);
						//log("sending updated rotation info");
					}
				} catch (e) {
					errorlog(e);
					if (session.pcs[UUID].startTime + 100000 < Date.now()) {
						warnlog("RTC Connection seems to be dead or not yet open? 8");
					} else {
						log("RTC Connection seems to be dead or not yet open? 8");
					}
				}
			}
		} catch (e) {
			errorlog(e);
		}

		if (!(Firefox && session.mobile)) {
			updateForceRotatedCSS();
		}
	} else if (Firefox && session.mobile) {
		try {
			var vs1 = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3");
			if (vs1) {
				vs1 = vs1.options[vs1.selectedIndex].textContent;
				if (vs1.includes(" back")) {
					FirefoxSucks = 2;
				} else if (vs1.includes(" front")) {
					FirefoxSucks = 1;
				}
			}

			if (screen && screen.orientation && screen.orientation.type) {
				if (screen.orientation.type.includes("portrait")) {
					session.forceRotate = 0;
				} else if (screen.orientation.type.includes("landscape")) {
					if (FirefoxSucks === 1) {
						session.forceRotate = 90;
					} else if (FirefoxSucks === 2) {
						session.forceRotate = 270;
					}
				}
			} else if (window.matchMedia("(orientation: portrait)").matches) {
				// legacy support; it seems to update late, 100ms or so after screen.orientation, so lets not use it
				session.forceRotate = 0;
			} else if (window.matchMedia("(orientation: landscape)").matches) {
				if (FirefoxSucks === 1) {
					session.forceRotate = 90;
				} else if (FirefoxSucks === 2) {
					session.forceRotate = 270;
				}
			}

			var msg = {};
			msg.rotate_video = 0;
			if (session.forceRotate !== false) {
				if (session.rotate) {
					msg.rotate_video = session.forceRotate + parseInt(session.rotate);
				} else {
					msg.rotate_video = session.forceRotate;
				}
				if (msg.rotate_video && msg.rotate_video >= 360) {
					msg.rotate_video -= 360;
				}
				//warnlog("FIREFOX MOBILE ONLY ROTATE: "+msg.rotate_video);
				//session.sendMessage(msg);
			} else {
				msg.rotate_video = parseInt(session.rotate) || 0;

				if (msg.rotate_video && msg.rotate_video >= 360) {
					msg.rotate_video -= 360;
				}
				//warnlog("FIREFOX MOBILE ONLY ROTATE: "+msg.rotate_video);
			}
			for (var UUID in session.pcs) {
				try {
					if (session.pcs[UUID].rotation != msg.rotate_video) {
						session.pcs[UUID].rotation = msg.rotate_video;
						session.sendMessage(msg, UUID);
						//log("sending updated rotation info");
					}
				} catch (e) {
					errorlog(e);
					if (session.pcs[UUID].startTime + 100000 < Date.now()) {
						warnlog("RTC Connection seems to be dead or not yet open? 8");
					} else {
						log("RTC Connection seems to be dead or not yet open? 8");
					}
				}
			}
		} catch (e) {
			errorlog(e);
		}
	} else {
		var msg = {};
		msg.rotate_video = 0;
		if (session.forceRotate !== false) {
			if (session.rotate) {
				msg.rotate_video = session.forceRotate + parseInt(session.rotate);
			} else {
				msg.rotate_video = session.forceRotate;
			}
		} else {
			msg.rotate_video = parseInt(session.rotate) || 0;
		}

		if (msg.rotate_video && msg.rotate_video >= 360) {
			msg.rotate_video -= 360;
		}

		for (var UUID in session.pcs) {
			try {
				if (session.pcs[UUID].rotation != msg.rotate_video) {
					session.pcs[UUID].rotation = msg.rotate_video;
					session.sendMessage(msg, UUID);
					//log("sending updated rotation info");
				}
			} catch (e) {
				errorlog(e);
				if (session.pcs[UUID].startTime + 100000 < Date.now()) {
					warnlog("RTC Connection seems to be dead or not yet open? 8");
				} else {
					log("RTC Connection seems to be dead or not yet open? 8");
				}
			}
		}
	}
	if (!skipLastBit) {
		applyMirror(session.mirrorExclude);
		session.setResolution(); // probably only triggers with mobile devices?
	}
}

function updateForceRotatedCSS(rotateThis = session.forceRotate) {
	if (rotateThis == 270) {
		document.body.setAttribute("style", "transform: rotate(270deg);position: absolute;top: 100vh;left: 0;height: 100vw;width: 100vh;transform-origin: 0 0;");
		document.body.dataset.rotated = "1";
	} else if (rotateThis == 90) {
		document.body.setAttribute("style", "transform: rotate(90deg);position: absolute;top: 0;left: 100vw;height: 100vw;width: 100vh;transform-origin: 0 0;");
		document.body.dataset.rotated = "1";
	} else if (rotateThis == 180) {
		document.body.setAttribute("style", "transform: rotate(180deg);position: absolute;top: 100vh;left: 100vw;height: 100vh;width: 100vw;transform-origin: 0 0;");
		document.body.dataset.rotated = "";
	} else {
		document.body.setAttribute("style", "");
		document.body.dataset.rotated = "";
	}
}

async function joinDataMode() {
	// join the room, but without publishing anything.
	await session.connect();
	if (session.roomid) {
		getById("head3").classList.add("hidden");
		getById("head3a").classList.add("hidden");
		joinRoom(session.roomid);
	} else if (session.view) {
		window.onresize = updateMixer;
		play();
		if (session.permaid !== false) {
			session.postPublish();
		}
	} else if (session.permaid !== false) {
		session.postPublish();
	}
}

function publishWebcam(btn = false, miconly = false) {
	if (btn) {
		if (btn.dataset.ready == "false") {
			warnlog("Clicked too quickly; button not enabled yet");
			return;
		}

		if (getById("passwordBasicInput").value.length) {
			session.password = getById("passwordBasicInput").value;
			session.password = sanitizePassword(session.password);
			if (session.password.length == 0) {
				session.password = false;
			} else {
				session.defaultPassword = false;
				if (urlParams.has("pass")) {
					updateURL("pass=" + session.password);
				} else if (urlParams.has("pw")) {
					updateURL("pw=" + session.password);
				} else if (urlParams.has("p")) {
					updateURL("p=" + session.password);
				} else {
					updateURL("password=" + session.password);
				}
			}
		}
	}

	if (activatedStream == true) {
		return;
	}
	activatedStream = true;
	log("PRESSED PUBLISH WEBCAM!!");

	formSubmitting = false;
	window.scrollTo(0, 0); // iOS has a nasty habit of overriding the CSS when changing camaera selections, so this addresses that.

	getById("head2").className = "hidden";

	if (session.mobile && !session.roomid && session.permaid === false) {
		if (!getById("rememberStreamID").classList.contains("hidden")) {
			if (getById("rememberStreamIDcheck").checked) {
				session.streamID = getStorage("permaid") || session.streamID;
				setStorage("permaid", session.streamID, 99999); // ~ 13 months?
				setStorage("rememberStreamIDmobile", "true", 99999);
			} else {
				removeStorage("permaid");
				setStorage("rememberStreamIDmobile", "false", 99999);
			}
		}
	}

	if (session.roomid !== false) {
		// they are in a room or a faux room

		window.onresize = updateMixer;
		window.onorientationchange = function () {
			if (Firefox) {
				updateForceRotate(true);
			}
			setTimeout(async function () {
				if (session.forceAspectRatio) {
					await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
				}
				if (session.effect && session.effect === "7") {
					digitalZoom(); // true needed to restart or start
				}
				updateForceRotate();
				updateMixer();
			}, 200);
		};

		if (session.roomid === "" && (!session.view || session.view === "")) {
			//	no room, no viewing, viewing disabled
			if (session.manual === null) {
				session.manual = session.manual === null ? true : session.manual;
			}
			if (!session.cleanOutput) {
				var showReshare = getStorage("showReshare");
				if (showReshare) {
					generateHash(session.streamID + session.salt + "bca321", 4)
						.then(function (hash) {
							// million to one error.
							if (showReshare === hash) {
								getById("head3").classList.remove("hidden");
								getById("head3a").classList.remove("hidden");
							} else if (session.permaid === null) {
								getById("head3").classList.remove("hidden");
								getById("head3a").classList.remove("hidden");
							}
						})
						.catch(errorlog);
				}
			}
		} else {
			log("ROOM ID ENABLED");
			log("Update Mixer Event on REsize SET");
			getById("main").style.overflow = "hidden";
			//session.cbr=0; // we're just going to override it

			if (session.stereo == 5) {
				if (session.roomid === "") {
					session.stereo = 1;
				} else {
					session.stereo = 3;
				}
			}
			joinRoom(session.roomid);
			if (session.roomid !== "") {
				if (!session.cleanOutput) {
					getById("head2").className = "";
				}
			}
			getById("head3").classList.add("hidden");
			getById("head3a").classList.add("hidden");
		}
	} else {
		// they are not in a room or faux room
		if (session.manual === null) {
			session.manual = session.manual === null ? true : session.manual;
		}
		getById("head3").classList.remove("hidden");
		getById("head3a").classList.remove("hidden");
		getById("logoname").style.display = "none";
		generateHash(session.streamID + session.salt + "bca321", 4)
			.then(function (hash) {
				// million to one error.
				setStorage("showReshare", hash, 24 * 30);
			})
			.catch(errorlog);
	}

	log("streamID is: " + session.streamID);
	getById("head1").className = "hidden";

	if (!session.cleanOutput) {
		getById("mutebutton").classList.remove("hidden");
		getById("mutespeakerbutton").classList.remove("hidden");
		//getById("mutespeakerbutton").className="float";
		getById("chatbutton").className = "float";
		getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though.
		getById("mutevideobutton").className = "float";
		getById("hangupbutton").className = "float";
		if (session.showSettings) {
			getById("settingsbutton").className = "float";
		}
		if (session.raisehands) {
			getById("raisehandbutton").className = "float";
		}
		if (session.pptControls) {
			getById("pptbackbutton").classList.remove("hidden");
			getById("pptnextbutton").classList.remove("hidden");
		}
		if (session.recordLocal !== false) {
			getById("recordLocalbutton").classList.remove("hidden");
		}
		if (session.screensharebutton) {
			if (session.roomid) {
				if (session.screenshareType === 3) {
					getById("screenshare3button").className = "float";
					getById("screensharebutton").className = "float hidden";
					getById("screenshare2button").className = "float hidden";
				} else if (session.screenshareType === 1) {
					getById("screensharebutton").className = "float";
					getById("screenshare3button").className = "float hidden";
					getById("screenshare2button").className = "float hidden";
				} else if (session.screenshareType === 2) {
					getById("screenshare2button").className = "float";
					getById("screensharebutton").className = "float hidden";
					getById("screenshare3button").className = "float hidden";
				} else {
					getById("screenshare3button").className = "float";
					getById("screensharebutton").className = "float hidden";
					getById("screenshare2button").className = "float hidden";
				}
			} else {
				getById("screensharebutton").className = "float";
				getById("screenshare2button").className = "float hidden";
				getById("screenshare3button").className = "float hidden";
			}
		}
		getById("controlButtons").classList.remove("hidden");
		//getById("helpbutton").style.display = "inherit";
		//getById("reportbutton").style.display = "";
	} else if (session.cleanish && session.recordLocal !== false) {
		getById("recordLocalbutton").classList.remove("hidden");
		getById("mutebutton").classList.add("hidden");
		getById("mutespeakerbutton").classList.add("hidden");
		getById("chatbutton").classList.add("hidden");
		getById("mutevideobutton").classList.add("hidden");
		getById("hangupbutton").classList.add("hidden");
		getById("hangupbutton2").classList.add("hidden");
		getById("controlButtons").classList.remove("hidden");
		getById("settingsbutton").classList.add("hidden");
		getById("screenshare2button").classList.add("hidden");
		getById("screensharebutton").classList.add("hidden");
		getById("queuebutton").classList.add("hidden");
	} else {
		getById("controlButtons").classList.add("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");
	}

	updatePushId();

	if (session.dataMode) {
		// skip the media stuff.
		errorlog("this shoulnd't happen..");
		session.postPublish();
		return;
	}

	if (!session.streamSrc) {
		checkBasicStreamsExist(); // create srcObject + videoElement
	}

	if (Firefox && session.mobile && session.streamSrc && needsLegacyWakeLock()) { // OBSOLETE since we now have "WAKE LOCK" API used.
		// this just keeps the phone active; firefox is more annoying

		setInterval(function () {
			if (document.getElementById("keepAlivePlayer") && session.streamSrc) {
				getById("keepAlivePlayer").remove();
			} else if (!document.getElementById("keepAlivePlayer")) {
				let fakeElement = document.createElement("video");
				fakeElement.autoplay = true;
				fakeElement.loop = true;
				fakeElement.muted = true;
				fakeElement.src = "./media/micro.mp4";
				fakeElement.style.width = "1px";
				fakeElement.style.height = "1px";
				fakeElement.controls = false;
				fakeElement.id = "keepAlivePlayer";
				getById("main").appendChild(fakeElement);
			}
		}, 4000);
	} else if (!session.avatar && session.mobile && session.streamSrc && !session.streamSrc.getVideoTracks().length && needsLegacyWakeLock()) {
		// this just keeps the phone active.  // OBSOLETE since we now have "WAKE LOCK" API used. 
		setInterval(function () {
			if (document.getElementById("keepAlivePlayer") && session.streamSrc.getVideoTracks().length) {
				getById("keepAlivePlayer").remove();
			} else if (!document.getElementById("keepAlivePlayer")) {
				let fakeElement = document.createElement("video");
				fakeElement.autoplay = true;
				fakeElement.loop = true;
				fakeElement.muted = true;
				fakeElement.src = "./media/micro.mp4";
				fakeElement.style.width = "1px";
				fakeElement.style.height = "1px";
				fakeElement.controls = false;
				fakeElement.id = "keepAlivePlayer";
				getById("main").appendChild(fakeElement);
			}
		}, 4000);
	}

	session.publishStream(getById("previewWebcam")); // calls session.postPublish at the end.
}

function createYoutubeLink(vidid) {
	return "https://www.youtube.com/embed/" + vidid + "?modestbranding=1&playsinline=1&enablejsapi=1&autoplay=1";
}
function parseURL4Iframe(iframeURL) {
	if (iframeURL == "") {
		iframeURL = "./";
	}
	if (iframeURL === session.iframeSrc) {
		return iframeURL;
	}

	if (!iframeURL.startsWith("https://") && !iframeURL.startsWith("http://")) {
		if (iframeURL.includes(".") && !iframeURL.startsWith("./") && !iframeURL.startsWith("/")) {
			iframeURL = "https://" + iframeURL;
		}
	}
	
	if (iframeURL.startsWith("http://")) {
		try {
			iframeURL = "https://" + iframeURL.split("http://")[1];
		} catch (e) {
			errorlog(e);
		}
	}

	if (iframeURL.startsWith("https://") || iframeURL.startsWith("http://")) {
		var domain;
		try {
			domain = new URL(iframeURL);
			domain = domain.hostname;
		} catch (e) {
			errorlog(e);
			return iframeURL;
		}

		if (domain == "youtu.be") {
			iframeURL = iframeURL.replace("youtu.be/", "youtube.com/watch?v=");
		}
		
		if (domain == "youtu.be" || domain == "www.youtube.com" || domain == "youtube.com") {
			if (iframeURL.includes("/v/")) {
				var vidMatch = iframeURL.match(/\/v\/([^\/\?#]+)/);
				if (vidMatch && vidMatch[1] && vidMatch[1].length == 11) {
					return createYoutubeLink(vidMatch[1]);
				}
			}
			
			var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
			var match = iframeURL.match(regExp);
			var vidid = match && match[7] && match[7].length == 11 ? match[7] : false;
			
			if (iframeURL.includes("/live_chat")) {
				if (!iframeURL.includes("&embed_domain=")) {
					iframeURL += "&embed_domain=" + location.hostname;
				}
				return iframeURL;
			}
			
			if (vidid) {
				iframeURL = createYoutubeLink(vidid);
			} else {
				iframeURL = iframeURL.replace("playlist?list=", "embed/videoseries?list=");
				var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(videoseries\?))\??list?=?([^#&?]*).*/;
				var match = iframeURL.match(regExp);
				var plid = match && match[7] && match[7].length == 34 ? match[7] : false;
				if (plid) {
					iframeURL = "https://www.youtube.com/embed/videoseries?list=" + plid + "&autoplay=1&modestbranding=1&playsinline=1&enablejsapi=1";
				}
			}
		} else if (domain == "twitch.tv" || domain == "www.twitch.tv") {
			if (iframeURL.includes("twitch.tv/popout/")) {
				iframeURL = iframeURL.replace("/popout/", "/embed/");
				iframeURL = iframeURL.replace("?popout=", "?parent=" + location.hostname);
				iframeURL = iframeURL.replace("?popout", "?parent=" + location.hostname);
				iframeURL = iframeURL.replace("&popout=", "?parent=" + location.hostname);
				iframeURL = iframeURL.replace("&popout", "?parent=" + location.hostname);
				if (iframeURL.includes("darkpopout=")) {
					iframeURL = iframeURL.replace("?darkpopout=", "?darkpopout=&parent=" + location.hostname);
				} else {
					iframeURL = iframeURL.replace("?darkpopout", "?darkpopout&parent=" + location.hostname);
				}
			} else {
				var vidid = iframeURL.split("/").pop().split("#")[0].split("?")[0];
				if (vidid) {
					iframeURL = "https://player.twitch.tv/?channel=" + vidid + "&parent=" + location.hostname;
				}
			}
		} else if (domain == "www.vimeo.com" || domain == "vimeo.com") {
			iframeURL = iframeURL.replace("//vimeo.com/", "//player.vimeo.com/video/");
			iframeURL = iframeURL.replace("//www.vimeo.com/", "//player.vimeo.com/video/");
		} else if (domain.endsWith(".tiktok.com") || domain == "tiktok.com") {
			var split = iframeURL.split("/video/");
			if (split.length > 1) {
				split = split[1].split("/")[0].split("?")[0].split("#")[0];
				iframeURL = "https://www.tiktok.com/embed/v2/" + split;
			}
		}
	}
	
	return iframeURL;
}

function soloLinkGenerator(streamID, scene = true) {
	var codecGroupFlag = "";
	if (session.codecGroupFlag) {
		codecGroupFlag = session.codecGroupFlag;
	}
	if (session.bitrateGroupFlag) {
		codecGroupFlag += session.bitrateGroupFlag;
	}

	var wss = "";
	if (session.wssSetViaUrl) {
		if (session.customWSS && (session.customWSS !== true)) {
			wss = "&pie=" + session.customWSS;
		} else if (session.customWSS==true){
			wss = "&wss=" + session.wss;
		} else {
			wss = "&wss2=" + session.wss;
		}
	}

	var passAdd2 = "";
	if (session.password) {
		if (session.defaultPassword === false) {
			passAdd2 = "&password=" + session.password;
		}
	}

	if (session.token) {
		passAdd2 += "&token=" + session.token;
	}

	if (scene) {
		return "https://" + location.host + location.pathname + "?view=" + streamID + "&solo" + codecGroupFlag + "&room=" + session.roomid + passAdd2 + wss + soloLinkAppended;
	} else {
		return "https://" + location.host + location.pathname + "?view=" + streamID + codecGroupFlag + passAdd2 + wss + soloLinkAppended;
	}
}

function YoutubeAPI(iframe, func, args) {
	// playVideo, pauseVideo, stopVideo
	if (!(iframe && iframe.contentWindow)) {
		return;
	}
	try {
		iframe.contentWindow.postMessage(
			JSON.stringify({
				event: "command",
				func: func,
				args: args || [],
				id: iframe.id || "unknown"
			}),
			"*"
		);
	} catch (e) {}
}

function YoutubeListen(iframe_id) {
	var iframe = document.getElementById(iframe_id);
	if (!iframe) {
		return;
	}
	if (iframe.loadedYoutubeListen) {
		return;
	}
	try {
		iframe.contentWindow.postMessage(JSON.stringify({ event: "listening", id: iframe_id }), "*"); //
	} catch (e) {}
	setTimeout(
		function (iframe_id) {
			YoutubeListen(iframe_id);
		},
		1000,
		iframe_id
	);
}

function processYoutubeEvent(e) {
	if (!(e.type && e.type === "message")) {
		return;
	}
	try {
		var data = JSON.parse(e.data);
		if ("id" in data) {
			var iframe = document.getElementById(data.id);
			if (!iframe) {
				return;
			}
			if (!iframe.loadedYoutubeListen) {
				iframe.loadedYoutubeListen = true;
			}
		}
		if (!("mediaReferenceTime" in data.info)) {
			return;
		}
	} catch (e) {
		return;
	}

	log(e);

	if (iframe.id == "iframe_source") {
		if (!session.iframeEle.sendOnNewConnect) {
			session.iframeEle.sendOnNewConnect = {};
			session.iframeEle.sendOnNewConnect.ifs = {};
			session.iframeEle.sendOnNewConnect.ifs.t = null;
			session.iframeEle.sendOnNewConnect.ifs.v = null;
			session.iframeEle.sendOnNewConnect.ifs.s = null;
			session.iframeEle.sendOnNewConnect.ifs.r = null;
		}

		try {
			var msg = {};
			msg.ifs = {};

			try {
				msg.ifs.t = parseFloat(data.info.mediaReferenceTime + 0.01) || 0;
				session.iframeEle.sendOnNewConnect.ifs.t = msg.ifs.t;
			} catch (e) {
				return;
			}

			if ("playerState" in data.info) {
				msg.ifs.s = parseInt(data.info.playerState);

				if (msg.ifs.s == -1) {
					msg.ifs.s = 0;
				}
				if (msg.ifs.s == 2) {
					if (session.iframeEle.sendOnNewConnect.ifs.s == 3) {
						delete msg.ifs.s;
					} else {
						msg.ifs.s = 3;
					}
				}
				if (msg.ifs.s && session.iframeEle.sendOnNewConnect.ifs.s != msg.ifs.s) {
					session.iframeEle.sendOnNewConnect.ifs.s = msg.ifs.s;
				} else {
					//delete(msg.ifs.s);
				}

				if ("videoData" in data.info) {
					if (session.iframeEle.sendOnNewConnect.ifs.v != data.info.videoData.video_id) {
						session.iframeEle.sendOnNewConnect.ifs.v = data.info.videoData.video_id;
						msg.ifs.v = data.info.videoData.video_id;

						var vidSrc = createYoutubeLink(msg.ifs.v);
						if (vidSrc !== session.iframeSrc) {
							session.iframeSrc = vidSrc;
							var data = {};
							data.iframeSrc = session.iframeSrc;
							if (parseInt(msg.ifs.t) > 1) {
								data.iframeSrc += "&start=" + parseInt(Math.ceil(msg.ifs.t)) + "";
							}

							for (var UUID in session.pcs) {
								if (session.pcs[UUID].allowIframe === true) {
									session.sendMessage(data, UUID);
								}
							}
							return;
						}
					}
				}
				// we will still be sending the msg data if available.
			} else if ("videoData" in data.info) {
				if (session.iframeEle.sendOnNewConnect.ifs.v != data.info.videoData.video_id) {
					msg.ifs.v = data.info.videoData.video_id;
					session.iframeEle.sendOnNewConnect.ifs.v = msg.ifs.v;
					var vidSrc = createYoutubeLink(msg.ifs.v);
					if (vidSrc !== session.iframeSrc) {
						session.iframeSrc = vidSrc;
						var data = {};
						data.iframeSrc = session.iframeSrc;
						if (parseInt(msg.ifs.t) > 1) {
							data.iframeSrc += "&start=" + parseInt(Math.ceil(msg.ifs.t)) + "";
						}
						if (session.iframeEle.sendOnNewConnect.ifs.s == "1") {
							data.iframeSrc += "&autoplay=1";
						} else {
							data.iframeSrc += "&autoplay=0";
						}
						for (var UUID in session.pcs) {
							if (session.pcs[UUID].allowIframe === true) {
								session.sendMessage(data, UUID);
							}
						}
						return;
					}
				}
			} else {
				if ("playbackRate" in data.info) {
					msg.ifs.r = parseFloat(data.info.playbackRate);
					if (session.iframeEle.sendOnNewConnect.ifs.r != msg.ifs.r) {
						session.iframeEle.sendOnNewConnect.ifs.r = msg.ifs.r;
					} else {
						delete msg.ifs.r;
					}
				}
				if (session.iframeEle.sendOnNewConnect.ifs.s == 1) {
					if ("t" in msg.ifs) {
						delete msg.ifs.t;
					}
				}
			}

			if (Object.keys(msg.ifs).length == 0) {
				return;
			}

			for (var UUID in session.pcs) {
				if (session.pcs[UUID].allowIframe) {
					session.sendMessage(msg);
				}
			}
		} catch (e) {
			return;
		}
	} else {
		try {
			var UUID = iframe.dataset.UUID;
			var msg = {};
			msg.ifs = {};
			if ("t" in msg.ifs) {
				msg.ifs.t = parseFloat(data.info.mediaReferenceTime + 0.01) || 0;
				/* if (!iframe.sendOnNewConnect){
					iframe.sendOnNewConnect = msg;
				} else {
					iframe.sendOnNewConnect.ifs.t = msg.ifs.t;
				} */
			}

			if ("playerState" in data.info) {
				msg.ifs.s = parseInt(data.info.playerState);
			}
			if ("videoData" in data.info) {
				msg.ifs.v = data.info.videoData.video_id;
			}
			if ("playbackRate" in data.info && data.info.playbackRate !== 1) {
				msg.ifs.r = parseFloat(data.info.playbackRate);
			}
			// TODO: the viewers don't have a way to tell the director if they reload what the time is at.
			session.sendRequest(msg, UUID); // send to the iframe's owner only. let them be the controller for others.
		} catch (e) {
			return;
		}
	}
}

function processIframeSyncFeedback(ifs, UUID) {
	// remote iframe feedback from the remote viewers
	// YoutubeAPI("iframe_source", "seekTo", [700]);
	// YoutubeAPI("iframe_source", "volume", [100]);

	warnlog(ifs);
	return;

	if (!session.iframeEle.sendOnNewConnect) {
		session.iframeEle.sendOnNewConnect = {};
		session.iframeEle.sendOnNewConnect.ifs = {};
		session.iframeEle.sendOnNewConnect.ifs.t = null;
		session.iframeEle.sendOnNewConnect.ifs.v = null;
		session.iframeEle.sendOnNewConnect.ifs.s = null;
		session.iframeEle.sendOnNewConnect.ifs.r = null;
	}

	if ("t" in ifs) {
		if (Math.abs(session.iframeEle.sendOnNewConnect.ifs.t - ifs.t) >= 1) {
			//session.iframeEle.sendOnNewConnect.ifs.t = ifs.t;
		} else {
			delete ifs.t;
		}
	}
	if ("v" in ifs) {
		if (session.iframeEle.sendOnNewConnect.ifs.v != ifs.v) {
			//session.iframeEle.sendOnNewConnect.ifs.v = ifs.v;
		} else {
			delete ifs.v;
		}
	}
	if ("s" in ifs) {
		if (ifs.s == -1) {
			ifs.s = 0;
		}
		if (session.iframeEle.sendOnNewConnect.ifs.s == -1) {
			session.iframeEle.sendOnNewConnect.ifs.s = 0;
		}

		if (ifs.s == 2) {
			ifs.s = 3;
		}
		if (session.iframeEle.sendOnNewConnect.ifs.s == 2) {
			session.iframeEle.sendOnNewConnect.ifs.s = 3;
		}

		if (session.iframeEle.sendOnNewConnect.ifs.s != ifs.s) {
			//session.iframeEle.sendOnNewConnect.ifs.s = ifs.s;
		} else {
			delete ifs.s;
		}
	}
	if ("r" in ifs) {
		if (session.iframeEle.sendOnNewConnect.ifs.r != ifs.r) {
			//session.iframeEle.sendOnNewConnect.ifs.r = ifs.r;
		} else {
			delete ifs.r;
		}
	}

	if (session.iframeEle) {
		if (ifs.v) {
			// I need to have this change videos .
			var vidSrc = createYoutubeLink(ifs.v);
			if (vidSrc !== session.iframeSrc) {
				session.iframeSrc = vidSrc;
				session.iframeEle.src = vidSrc;
			}
		} else if ("t" in ifs) {
			YoutubeAPI(session.iframeEle, "seekTo", [parseFloat(ifs.t)]);
		} else if (ifs.r) {
			/// setPlaybackRate
			YoutubeAPI(session.iframeEle, "setPlaybackRate", [parseFloat(ifs.r)]);
		} else if ("s" in ifs) {
			/// setPlaybackState
			if (ifs.s == -1) {
				YoutubeAPI(session.iframeEle, "stopVideo");
			} else if (ifs.s == 0) {
				YoutubeAPI(session.iframeEle, "stopVideo");
			} // player stops.
			else if (ifs.s == 1) {
				YoutubeAPI(session.iframeEle, "playVideo");
			} //Video is playing
			else if (ifs.s == 2) {
				YoutubeAPI(session.iframeEle, "pauseVideo");
			} //Video is paused
			else if (ifs.s == 3) {
				YoutubeAPI(session.iframeEle, "pauseVideo");
			} //video is buffering
			else if (ifs.s == 5) {
			} //Video is cued.
		}
	} else if (session.iframeSrc) {
		if (ifs.v) {
			var vidSrc = createYoutubeLink(ifs.v);
			if (vidSrc !== session.iframeSrc) {
				session.iframeSrc = vidSrc;
				var data = {};
				data.iframeSrc = session.iframeSrc;
				if (ifs.t && parseInt(ifs.t) > 1) {
					data.iframeSrc += "&start=" + parseInt(Math.ceil(ifs.t));
				}
				if (ifs.s == "1") {
					data.iframeSrc += "&autoplay=1";
				} else {
					data.iframeSrc += "&autoplay=0";
				}
				for (var uuid in session.pcs) {
					if (uuid == UUID) {
						continue;
					}
					if (session.pcs[uuid].allowIframe === true) {
						session.sendMessage(data, uuid);
					}
				}
				return;
			}
		}
		// we're going to forward the message directly to the other viewers instead
		if ("s" in ifs) {
			/// setPlaybackState
			var msg = {};
			msg.ifs = ifs;
			for (var uuid in session.pcs) {
				if (uuid == UUID) {
					continue;
				}
				if (session.pcs[uuid].allowIframe) {
					session.sendMessage(msg, uuid);
				}
			}
		}
	}
}

function processIframeSyncUpdates(ifs, UUID) {
	// playback updates from remote guest.
	// YoutubeAPI("iframe_source", "seekTo", [700]);
	// YoutubeAPI("iframe_source", "volume", [100]);
	if (ifs.v && "s" in ifs) {
		//
	} else if ("s" in ifs) {
		if ("t" in ifs) {
			YoutubeAPI(session.rpcs[UUID].iframeEle, "seekTo", [parseFloat(ifs.t)]);
		}
		YoutubeAPI(session.rpcs[UUID].iframeEle, "playVideo");
	} else if ("t" in ifs) {
		YoutubeAPI(session.rpcs[UUID].iframeEle, "seekTo", [parseFloat(ifs.t)]);
	}
	if (ifs.r) {
		/// setPlaybackRate
		YoutubeAPI(session.rpcs[UUID].iframeEle, "setPlaybackRate", [parseFloat(ifs.r)]);
	}
	if ("s" in ifs) {
		/// setPlaybackState
		if (ifs.s == -1) {
			YoutubeAPI(session.rpcs[UUID].iframeEle, "stopVideo");
		} else if (ifs.s == 0) {
			YoutubeAPI(session.rpcs[UUID].iframeEle, "stopVideo");
		} // player stops.
		else if (ifs.s == 1) {
			YoutubeAPI(session.rpcs[UUID].iframeEle, "playVideo");
		} //Video is playing
		else if (ifs.s == 2) {
			YoutubeAPI(session.rpcs[UUID].iframeEle, "pauseVideo");
		} //Video is paused
		else if (ifs.s == 3) {
			YoutubeAPI(session.rpcs[UUID].iframeEle, "pauseVideo");
		} //video is buffering
		else if (ifs.s == 5) {
		} //Video is cued.
	}
}

function updatePushId() {
	if (session.doNotSeed) {
		return;
	}

	if (urlParams.has("push")) {
		updateURL("push=" + session.streamID);
	} else if (urlParams.has("id")) {
		updateURL("id=" + session.streamID);
	} else if (urlParams.has("permaid")) {
		updateURL("permaid=" + session.streamID);
	} else {
		updateURL("push=" + session.streamID);
	}
}

session.publishIFrame = function (iframeURL) {
	if (!session.cleanOutput) {
		getById("websitesharebutton2").classList.remove("hidden");
	}

	if (session.transcript) {
		setTimeout(function () {
			setupClosedCaptions();
		}, 1000);
	}

	session.iframeSrc = parseURL4Iframe(iframeURL);
	
	if (!session.iFramesAllowed){errorlog("Can't create iFRAME - security is tainted due to possible CSS injection");warnUser("Can't create iFRAME - security is tainted due to possible CSS injection");return;}

	var iframe = document.createElement("iframe");
	iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location
	iframe.src = session.iframeSrc;
	iframe.id = "iframe_source";
	iframe.setAttribute("allowtransparency", "true");
	iframe.setAttribute("crossorigin", "anonymous");
	iframe.setAttribute("credentialless", "true");
	iframe.loadedYoutubeListen = false;
	session.iframeEle = iframe;

	var container = document.createElement("div");
	iframe.container = container;
	container.id = "container_iframe";
	container.appendChild(iframe);
	getById("gridlayout").appendChild(container);

	if (session.iframeSrc.startsWith("https://www.youtube.com/")) {
		// special handler.
		setTimeout(
			function (iframe_id) {
				YoutubeListen(iframe_id);
			},
			1000,
			iframe.id
		);
	}

	if (session.cover) {
		container.style.setProperty("height", "100%", "important");
	}

	if (session.roomid !== false) {
		if (session.roomid === "" && (!session.view || session.view === "")) {
		} else {
			log("ROOMID EANBLED");
			getById("head3").classList.add("hidden");
			getById("head3a").classList.add("hidden");
			joinRoom(session.roomid);
		}
	} else {
		getById("head3").classList.remove("hidden");
		getById("head3a").classList.remove("hidden");
		getById("logoname").style.display = "none";
	}
	getById("head1").className = "hidden";

	updatePushId();

	getById("head1").className = "hidden";
	getById("head2").className = "hidden";

	if (!session.cleanOutput) {
		getById("chatbutton").className = "float";
		getById("hangupbutton").className = "float";
		getById("controlButtons").classList.remove("hidden");
		getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though.
		//getById("helpbutton").style.display = "inherit";
		//getById("reportbutton").style.display = "";
	} else {
		getById("controlButtons").classList.add("hidden");
	}

	if (session.chatbutton === false) {
		getById("chatbutton").classList.add("hidden");
	}

	if (session.director) {
		//
	} else if (session.scene !== false) {
		updateMixer();
	} else if (session.roomid !== false) {
		if (session.roomid === "") {
			if (!session.view || session.view === "") {
				session.windowed = session.windowed === null ? true : session.windowed;
				container.classList.add("vidcon");

				getById("mutespeakerbutton").classList.add("hidden");
				container.style.width = "100%";
				container.style.height = "100%";
				container.style.alignItems = "center";
				container.style.maxWidth = "100%";
				container.style.maxHeight = "100%";
				container.style.verticalAlign = "middle";
				container.style.margin = "auto";
				container.style.backgroundColor = "#666";
				container.style.border = "2px solid";
			} else {
				session.windowed = session.windowed === null ? false : session.windowed;
				window.onresize = updateMixer;
				updateMixer();
			}
		} else {
			window.onresize = updateMixer;
			session.windowed = session.windowed === null ? false : session.windowed;
			updateMixer();
		}
	} else {
		window.onresize = updateMixer;
		container.style.maxHeight = "1280px";
		container.style.maxWidth = "720px";
		container.style.verticalAlign = "middle";
		container.style.height = "100%";
		container.style.width = "100%";
		container.style.margin = "auto";
		container.style.alignItems = "center";
		container.style.backgroundColor = "#666";
	}

	session.seeding = true;

	updateReshareLink();
	pokeIframeAPI("started-iframe-share");
	session.seedStream();

	return container;
}; // publishIframe

/* session.publishWhepSrc = function(){
	
	if (!session.whepSrc){errorlog("no WHEP Src");return;}
	
	if (!session.cleanOutput){
		getById("websitesharebutton2").classList.remove('hidden');
	}
	
	var UUID = whepIn(session.whepSrc);
	
	var container = document.createElement("div");
	iframe.container = container;
	container.id = "container_iframe";
	container.appendChild(iframe);
	getById("gridlayout").appendChild(container);
	
	if (session.iframeSrc.startsWith("https://www.youtube.com/")){ // special handler.
		setTimeout(function(iframe_id){YoutubeListen(iframe_id);}, 1000, iframe.id);
	} 
	
	if (session.cover){
		container.style.setProperty('height', '100%', 'important');
	}
	
	if (session.roomid!==false){
		if ((session.roomid==="") && ((!(session.view)) || (session.view===""))){
			
		} else {
			log("ROOMID EANBLED");
			getById("head3").classList.add('hidden');
			getById("head3a").classList.add('hidden');
			joinRoom(session.roomid);
		}
		
	} else {
		getById("head3").classList.remove('hidden');
		getById("head3a").classList.remove('hidden');
		getById("logoname").style.display = 'none';
	}
	getById("head1").className = 'hidden';
	
	updatePushId()
	
	getById("head1").className = 'hidden';
	getById("head2").className = 'hidden';

	if (!(session.cleanOutput)){
		getById("chatbutton").className="float";
		getById("hangupbutton").className="float";
		getById("controlButtons").classList.remove("hidden");
		getById('sharefilebutton').classList.remove("hidden"); // we won't override "display:none", if set, though.
		getById("helpbutton").style.display = "inherit";
		getById("reportbutton").style.display = "";
	} else {
		getById("controlButtons").classList.add("hidden");
	}
	
	if (session.chatbutton === false) {
		getById("chatbutton").classList.add("hidden");
	}
	
	if (session.director){
		//
	} else if (session.scene!==false){
		updateMixer();
	} else if (session.roomid!==false){
		if (session.roomid===""){
			if (!session.fullscreen && (!(session.view) || (session.view===""))){
				session.windowed = session.windowed === null ? true : session.windowed;
				container.classList.add("vidcon");
				
				getById("mutespeakerbutton").classList.add("hidden");
				container.style.width="100%";
				container.style.height="100%";
				container.style.alignItems = "center";
				container.style.maxWidth= "100%";
				container.style.maxHeight= "100%";
				container.style.verticalAlign= "middle";
				container.style.margin= "auto";
				container.style.backgroundColor = "#666";
				container.style.border = "2px solid";
				
			} else {
				session.windowed = session.windowed === null ? false : session.windowed;
				window.onresize = updateMixer;
				updateMixer();
			}
		} else {
			window.onresize = updateMixer;
			session.windowed = session.windowed === null ? false : session.windowed;
			updateMixer();
		}
	} else {
		window.onresize = updateMixer;
		container.style.maxHeight= "1280px";
		container.style.maxWidth= "720px";
		container.style.verticalAlign= "middle";
		container.style.height="100%";
		container.style.width= "100%";
		container.style.margin= "auto";
		container.style.alignItems = "center";
		container.style.backgroundColor = "#666";
	}
	
	session.seeding=true;
	
	updateReshareLink();
	pokeIframeAPI('started-iframe-share');
	session.seedStream();
	
	return container;
} // publishWhepSrc */



function disabledWebAudioPathway() {
	log("Executing disabledWebAudioPathway.");
	// if (session.disableWebAudio) { then run this instead; or if webaudio nodes fail.}
	// if (iOS || iPad){return session.streamSrc;} // Original comment: iOS devices can't remap video tracks, else KABOOM. Might as well do this for android also.
	// This iOS specific return was in comments, if it's critical, it should be uncommented.
	// However, the logic below also attempts to handle stream cloning.

	if (session.streamSrcClone) {
		log("disabledWebAudioPathway: Cleaning up existing session.streamSrcClone");
		session.streamSrcClone.getTracks().forEach(function (track) {
			session.streamSrcClone.removeTrack(track);
			track.stop();
		});
		session.streamSrcClone = null;
	}

	var newStream = createMediaStream(); // This will be the returned stream

	if (session.streamSrc && typeof session.streamSrc.clone === 'function' && !window.obsstudio) { // Prefer cloning if available and not obsstudio
		log("disabledWebAudioPathway: Cloning session.streamSrc (non-obsstudio path)");
		newStream = session.streamSrc.clone();
	} else {
		log("disabledWebAudioPathway: Creating new stream and adding tracks manually.");
		if (session.streamSrc) {
			session.streamSrc.getAudioTracks().forEach(function (track) {
				if (track.readyState === 'live') {
					// For obsstudio, audio tracks can also be cloned for consistency, though less critical than video.
					// For simplicity here, adding original, but could be `track.clone()`
					newStream.addTrack(track);
					log("disabledWebAudioPathway: Added audio track " + track.id);
				}
			});
		}

		let videoSourceForDisabledPath = null;
		if (session.videoElement && session.videoElement.srcObject) {
			videoSourceForDisabledPath = session.videoElement.srcObject;
		} else if (session.streamSrc) {
			videoSourceForDisabledPath = session.streamSrc;
		}

		if (videoSourceForDisabledPath) {
			videoSourceForDisabledPath.getVideoTracks().forEach(function (track) {
				if (track.readyState === 'live') {
					if (window.obsstudio) {
						log(`disabledWebAudioPathway (obsstudio): Cloning video track ${track.id}`);
						try {
							const clonedVideoTrack = track.clone();
							newStream.addTrack(clonedVideoTrack);
						} catch (e_clone_video) {
							errorlog(`disabledWebAudioPathway (obsstudio): Failed to clone video track ${track.id}. Adding original. Error:`, e_clone_video);
							newStream.addTrack(track); // Fallback
						}
					} else {
						log(`disabledWebAudioPathway (non-obsstudio): Adding original video track ${track.id}`);
						newStream.addTrack(track); // Original behavior
					}
				}
			});
		}
	}
	if (iOS || iPad || session.streamSrcClone) {
		session.streamSrcClone = newStream; // Store the newly created/cloned stream
	}
	return newStream;
}

function outboundAudioPipeline(sourceStream = false) {

	if (session.disableWebAudio) {
		return disabledWebAudioPathway(); // Safemode
	}

	if (!session.streamSrc && !sourceStream) {
		errorlog("STREAM DOES NOT EXIST. This is a problem");
		checkBasicStreamsExist();
		return session.streamSrc;
	}

	var streamSrc = sourceStream || session.streamSrc;

	if (iOS || iPad) {
		if (session.streamSrcClone) {
			var audioTracksForCleanup = session.streamSrcClone.getAudioTracks();
			if (audioTracksForCleanup.length) {
				for (var waid in session.webAudios) {
					if (session.webAudios[waid] && typeof session.webAudios[waid].stop === 'function') {
						session.webAudios[waid].stop();
					}
					delete session.webAudios[waid];
				}
			}
			session.streamSrcClone.getTracks().forEach(function (track) {
				session.streamSrcClone.removeTrack(track);
				track.stop();
			});
			session.streamSrcClone = null;
		}

		// Create iOS-compatible stream (always clone for iOS)
		if (session.streamSrc && typeof session.streamSrc.clone === 'function') {
			log("iOS: Cloning session.streamSrc");
			streamSrc = session.streamSrc.clone();
			session.streamSrcClone = streamSrc;
		} else {
			log("iOS: Creating new stream as backup");
			session.streamSrcClone = createOptimizedStream(session.streamSrc, true);
			streamSrc = session.streamSrcClone;
		}
	}

	for (var waid in session.webAudios) {
		if (session.webAudios[waid] && typeof session.webAudios[waid].stop === 'function') {
			session.webAudios[waid].stop();
		}
		delete session.webAudios[waid];
	}
	session.webAudios = session.webAudios || {};

	try {
		log("Web Audio processing initiated.");
		var audioTracks = streamSrc.getAudioTracks();

		if (audioTracks.length) {
			var webAudio = initWebAudioNode(audioTracks[0].id);
			
			if (audioTracks.length > 1) {
				try {
					setupMultiTrackAudio(audioTracks, webAudio);
				} catch (e_multi) {
					errorlog("Error in multi-track audio setup, falling back: ", e_multi);
					try {
						webAudio.mediaStreamSource = webAudio.audioContext.createMediaStreamSource(streamSrc);
						webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext);
					} catch (e_multi_fallback) {
						errorlog("Fallback failed: ", e_multi_fallback);
						return disabledWebAudioPathway();
					}
				}
			} else {
				try {
					webAudio.mediaStreamSource = webAudio.audioContext.createMediaStreamSource(streamSrc); 
					webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext);
				} catch (e_single) {
					errorlog("Error creating single track setup: ", e_single);
					return disabledWebAudioPathway();
				}
			}

			var anonNode = applyAudioProcessing(webAudio, streamSrc);

			const finalOutputStream = createMediaStream();
			webAudio.destination.stream.getAudioTracks().forEach(audioTrack => {
				finalOutputStream.addTrack(audioTrack);
			});

			addVideoTracksToStream(finalOutputStream, streamSrc);

			if (webAudio.audioContext && webAudio.audioContext.state === "suspended") {
				webAudio.audioContext.resume().catch(e => errorlog("AudioContext resume failed:", e));
			}
			
			return finalOutputStream;

		} else { 
			log("No audio tracks found. Handling video passthrough.");
			// Return video-only stream
			if (window.obsstudio) {
				log("OBS (no audio): Creating stream with cloned video tracks");
				const newStream = createMediaStream();
				addVideoTracksToStream(newStream, streamSrc);
				return newStream;
			} else {
				log("Non-OBS (no audio): Using direct video source");
				if (session.videoElement && session.videoElement.srcObject) {
					return session.videoElement.srcObject;
				}
				
				const newStream = createMediaStream();
				addVideoTracksToStream(newStream, streamSrc);
				return newStream;
			}
		}
	} catch (e_main) {
		errorlog("Critical error in outboundAudioPipeline: ");
		errorlog(e_main);
		return streamSrc;
	}
}

function createOptimizedStream(source, shouldClone = false) {
	const newStream = createMediaStream();
	
	if (!source) return newStream;
	
	source.getAudioTracks().forEach(track => {
		if (track.readyState === 'live') {
			if (shouldClone) {
				try {
					newStream.addTrack(track.clone());
				} catch (e) {
					errorlog("Failed to clone audio track. Adding original. Error:", e);
					newStream.addTrack(track);
				}
			} else {
				newStream.addTrack(track);
			}
		}
	});
	
	addVideoTracksToStream(newStream, source);
	
	return newStream;
}

function addVideoTracksToStream(targetStream, sourceStream) {
	let videoSourceStream = sourceStream;
	
	// Find video source with fallback
	if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length > 0) {
		videoSourceStream = session.videoElement.srcObject;
	} else if (!videoSourceStream || videoSourceStream.getVideoTracks().length === 0) {
		videoSourceStream = session.streamSrc;
	}
	
	if (videoSourceStream) {
		videoSourceStream.getVideoTracks().forEach(track => {
			if (track.readyState === 'live') {
				// Only clone for OBS Studio
				if (window.obsstudio) {
					log(`OBS: Cloning video track ${track.id}`);
					try {
						const clonedTrack = track.clone();
						targetStream.addTrack(clonedTrack);
					} catch (e_clone) {
						errorlog(`OBS: Failed to clone track ${track.id}. Adding original. Error:`, e_clone);
						targetStream.addTrack(track);
					}
				} else {
					targetStream.addTrack(track);
				}
			}
		});
	}
}

function initWebAudioNode(trackId) {
	var webAudio = {
		id: trackId,
		micDelay: false,
		compressor: false,
		analyser: false,
		gainNode: false,
		splitter: false,
		subGainNodes: false,
		lowEQ: false,
		midEQ: false,
		highEQ: false,
		lowcut1: false,
		lowcut2: false,
		lowcut3: false,
		waveShaper_vc: null,
		oscillator_vc: null,
		oscillatorGain_vc: null,
		delay_vc: null,
		lowEQ_vc: null,
		mid_vc: null
	};

	// Create audio context if needed
	if (session.audioCtxOutbound) {
		// Already created
	} else if (session.outboundSampleRate) {
		try {
			session.audioCtxOutbound = new AudioContext({ sampleRate: session.outboundSampleRate });
		} catch (e) {
			session.audioCtxOutbound = new AudioContext();
			errorlog(e);
		}
	} else if (session.outboundSampleRate === false || Firefox || SafariVersion || session.mobile) {
		session.audioCtxOutbound = new AudioContext();
	} else if (session.audioLatency !== false) {
		session.audioCtxOutbound = new AudioContext({
			latencyHint: session.audioLatency / 1000.0,
			sampleRate: 48000
		});
	} else {
		try {
			session.audioCtxOutbound = new AudioContext({ sampleRate: 48000 });
		} catch (e) {
			session.audioCtxOutbound = new AudioContext();
			errorlog(e);
		}
	}

	if (session.audioCtxOutbound && session.audioCtxOutbound.sampleRate > 192000) {
		console.error("Warning: Your audio playback device has a very high sample rate set; lower it to 48000-Hz to avoid audio issues");
	}

	webAudio.audioContext = session.audioCtxOutbound;
	webAudio.destination = session.audioCtxOutbound.createMediaStreamDestination();
	
	return webAudio;
}

// Helper function to handle multi-track audio setup
function setupMultiTrackAudio(audioTracks, webAudio) {
	var maxChannelCount = session.stereo === false ? 1 : 2;
	webAudio.subGainNodes = {};
	
	var mergerNode = webAudio.audioContext.createChannelMerger(maxChannelCount);
	
	for (var i = 0; i < audioTracks.length; i++) {
		try {
			var tempIndividualTrackStream = createMediaStream();
			tempIndividualTrackStream.addTrack(audioTracks[i]);
			var trackAudioSourceNode = webAudio.audioContext.createMediaStreamSource(tempIndividualTrackStream);
			
			webAudio.subGainNodes[audioTracks[i].id] = webAudio.audioContext.createGain();
			trackAudioSourceNode.connect(webAudio.subGainNodes[audioTracks[i].id]);
			
			if (maxChannelCount == 2) {
				var individualSplitter = webAudio.audioContext.createChannelSplitter(2);
				webAudio.subGainNodes[audioTracks[i].id].connect(individualSplitter);
				individualSplitter.connect(mergerNode, 0, 0);
				try {
					individualSplitter.connect(mergerNode, 1, 1);
				} catch (e_stereo) {
					errorlog("Stereo connect ch1->input1 failed: ", e_stereo);
					try {
						individualSplitter.connect(mergerNode, 0, 1);
					} catch (e_stereo_fallback) {
						errorlog("Stereo connect ch0->input1 fallback failed: ", e_stereo_fallback);
					}
				}
			} else {
				webAudio.subGainNodes[audioTracks[i].id].connect(mergerNode, 0, 0);
			}
		} catch (e_track) {
			errorlog("Error processing track: ", e_track);
			throw e_track;
		}
	}
	
	webAudio.mediaStreamSource = mergerNode;
	webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext);
}

function applyAudioProcessing(webAudio, streamSrc) {
	try {
		var anonNode = webAudio.gainNode;
		
		// Channel downmixing
		if (session.audioInputChannels == 1) {
			anonNode = applyDownmixing(anonNode, webAudio);
		}
		
		// Low cut filter
		if (session.lowcut) {
			anonNode = applyLowCut(anonNode, webAudio);
		}
		
		// Voice changer
		if (session.voicechanger) {
			anonNode = applyVoiceChanger(anonNode, webAudio);
		}
		
		// Equalizer
		if (session.equalizer) {
			anonNode = applyEqualizer(anonNode, webAudio);
		}
		
		// Compressor/Limiter
		if (session.compressor === 1) {
			webAudio.compressor = audioCompressor(anonNode, webAudio.audioContext);
			anonNode = webAudio.compressor;
		} else if (session.compressor === 2) {
			webAudio.compressor = audioLimiter(anonNode, webAudio.audioContext);
			anonNode = webAudio.compressor;
		}
		
		// Mic delay
		if (session.micDelay !== false) {
			webAudio.micDelay = micDelayNode(anonNode, webAudio.audioContext);
			anonNode = webAudio.micDelay;
		}
		
		// Twilio mix
		if (session.twilio && session.twilio.element && session.twilio.element.srcObject && session.twilio.element.srcObject.getAudioTracks().length) {
			const twilioSource = webAudio.audioContext.createMediaStreamSource(session.twilio.element.srcObject);
			twilioSource.connect(anonNode);
		}
		
		// Noise gate
		if (session.noisegate !== false) {
			webAudio.analyser = audioMeter(anonNode, webAudio.audioContext);
			anonNode = webAudio.analyser;
			webAudio.gatingNode = audioGatingNode(anonNode, webAudio.audioContext);
			webAudio.gatingNode.connect(webAudio.destination);
		} else {
			webAudio.analyser = audioMeter(anonNode, webAudio.audioContext);
			webAudio.analyser.connect(webAudio.destination);
		}
		
		webAudio.stop = createStopFunction(webAudio);
		
		if (streamSrc && webAudio.mediaStreamSource) {
			const tracks = streamSrc.getTracks();
			if (tracks.length) {
			  tracks.forEach(track => {
				track.addEventListener('ended', () => {
				  log("Track ended, stopping webAudio");
				  webAudio.stop();
				});
			  });
			} else if (webAudio.mediaStreamSource.onended !== undefined) {
			  // Fallback for older browsers
			  webAudio.mediaStreamSource.onended = () => {
				log("MediaStreamSource ended, stopping webAudio");
				webAudio.stop();
			  };
			}
		}
		
		session.webAudios[webAudio.id] = webAudio;
	} catch(e){
		console.error(e);
		return webAudio;
	}
	
	return anonNode;
}
function createStopFunction(webAudio) {
  return function() {
    // Prevent multiple calls
    if (webAudio.stopped) {
      errorlog("Trying to stop webaudio more than once");
      return;
    }
    webAudio.stopped = true;
    
    // Clear analyzer interval if it exists
    try {
      if (webAudio.analyser && webAudio.analyser.interval) {
        clearInterval(webAudio.analyser.interval);
      }
    } catch (e) {
      errorlog("Error clearing analyser interval:", e);
    }
    
    // Special handling for subGainNodes (collection of nodes)
    if (webAudio.subGainNodes) {
      for (var id in webAudio.subGainNodes) {
        try {
          if (webAudio.subGainNodes[id]) {
            webAudio.subGainNodes[id].disconnect();
            webAudio.subGainNodes[id] = null;
          }
        } catch (e) {
          errorlog("Error disconnecting subGainNode " + id + ":", e);
        }
      }
      webAudio.subGainNodes = null;
    }
    
    // List of properties to skip disconnecting
    const skipProperties = ["stop", "id", "audioContext", "mediaStreamSource", "subGainNodes", "stopped"];
    
    // Disconnect all other nodes
    for (var node in webAudio) {
      if (!webAudio[node] || skipProperties.includes(node)) {
        continue;
      }
      
      try {
        // Only disconnect if it has a disconnect method (is an audio node)
        if (typeof webAudio[node].disconnect === 'function') {
          webAudio[node].disconnect();
          log("Disconnected node: " + node);
        }
        webAudio[node] = null;
      } catch (e) {
        errorlog("Error disconnecting node " + node + ":", e);
      }
    }
    
    // Remove from session tracking
    if (session.webAudios && webAudio.id && session.webAudios[webAudio.id]) {
      delete session.webAudios[webAudio.id];
    }
  };
}



function changeLowCut(freq, deviceid = null) {
	log("LOW EQ");

	for (var webAudio in session.webAudios) {
		if (!session.webAudios[webAudio].lowcut1) {
			errorlog("EQ not setup");
			return;
		}
		if (!session.webAudios[webAudio].lowcut2) {
			errorlog("EQ not setup");
			return;
		}
		if (!session.webAudios[webAudio].lowcut3) {
			errorlog("EQ not setup");
			return;
		}
		session.webAudios[webAudio].lowcut1.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime);
		session.webAudios[webAudio].lowcut2.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime);
		session.webAudios[webAudio].lowcut3.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime);
	}
}

function changeLowEQ(lowEQ, deviceid = null) {
	log("LOW EQ");

	for (var webAudio in session.webAudios) {
		if (!session.webAudios[webAudio].lowEQ) {
			errorlog("EQ not setup");
			return;
		}
		session.webAudios[webAudio].lowEQ.gain.setValueAtTime(lowEQ, session.webAudios[webAudio].audioContext.currentTime);
	}
}

function changeMidEQ(midEQ, deviceid = null) {
	for (var webAudio in session.webAudios) {
		if (!session.webAudios[webAudio].midEQ) {
			errorlog("EQ not setup");
			return;
		}
		session.webAudios[webAudio].midEQ.gain.setValueAtTime(midEQ, session.webAudios[webAudio].audioContext.currentTime);
	}
}

function changeHighEQ(highEQ, deviceid = null) {
	for (var webAudio in session.webAudios) {
		if (!session.webAudios[webAudio].highEQ) {
			errorlog("EQ not setup");
			return;
		}
		session.webAudios[webAudio].highEQ.gain.setValueAtTime(highEQ, session.webAudios[webAudio].audioContext.currentTime);
	}
}

function changeMicDelay(delay, deviceid = null) {
	log("changeMicDelay :" + delay);
	for (var waid in session.webAudios) {
		// add a mic delay
		if (!session.webAudios[waid].micDelay) {
			errorlog("Mic Delay not setup");
		} else {
			session.webAudios[waid].micDelay.delayTime.setValueAtTime(delay / 1000, session.webAudios[waid].audioContext.currentTime);
		}
	}
}

function changeSubGain(gain, deviceid = null) {
	if (gain !== false) {
		gain = parseFloat(gain / 100.0) || 0;
	} else {
		gain = 1.0;
	}
	for (var webAudio in session.webAudios) {
		try {
			if (!session.webAudios[webAudio].subGainNodes) {
				errorlog("EQ not setup");
				return;
			}
			if (deviceid in session.webAudios[webAudio].subGainNodes) {
				session.webAudios[webAudio].subGainNodes[deviceid].gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime);
			} else {
				errorlog("NOT FOUND:" + deviceid);
			}
			break;
		} catch (e) {
			errorlog(e);
		}
	}
}

function changeMainGain(gain, fadeout = 0) {
	for (var webAudio in session.webAudios) {
		if (!session.webAudios[webAudio].gainNode) {
			return;
		}
		if (gain !== false) {
			gain = parseFloat(gain / 100.0) || 0;
		} else {
			gain = 1.0;
		}
		if (fadeout) {
			try {
				session.webAudios[webAudio].gainNode.gain.linearRampToValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime + fadeout / 1000);
			} catch (e) {
				session.webAudios[webAudio].gainNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime);
			}
		} else {
			session.webAudios[webAudio].gainNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime);
		}
	}
}

function changeGatingGain(gain, fadeout = 0) {
	for (var webAudio in session.webAudios) {
		if (!session.webAudios[webAudio].gatingNode) {
			return;
		}
		if (gain !== false) {
			gain = parseFloat(gain / 100.0) || 0;
		} else {
			gain = 1.0;
		}
		if (fadeout) {
			try {
				session.webAudios[webAudio].gatingNode.gain.linearRampToValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime + fadeout / 1000);
			} catch (e) {
				session.webAudios[webAudio].gatingNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime);
			}
		} else {
			session.webAudios[webAudio].gatingNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime);
		}
	}
}

function applyDownmixing(inputNode, webAudio) {
	// Complex downmixing logic with channel counting and gain adjustment
	let totalChannels = 0;
	let activeChannels = 0;
	let tracks = webAudio.audioContext._stream ? webAudio.audioContext._stream.getAudioTracks() : [];

	tracks.forEach(track => {
		if (track.getSettings && track.getSettings().channelCount) {
			let trackChannels = track.getSettings().channelCount;
			totalChannels += trackChannels;
			if (track.enabled) {
				activeChannels += trackChannels;
			}
		} else {
			// Fallback if getSettings is not available
			totalChannels += 2; // Assume stereo
			if (track.enabled) {
				activeChannels += 2;
			}
		}
	});

	totalChannels = Math.max(totalChannels, 1);
	activeChannels = Math.max(activeChannels, 1);

	webAudio.splitter = webAudio.audioContext.createChannelSplitter(totalChannels);
	inputNode.connect(webAudio.splitter);
	webAudio.merger = webAudio.audioContext.createChannelMerger(1);

	// Create a gain node for volume adjustment
	webAudio.downmixGain = webAudio.audioContext.createGain();

	// Connect splitter outputs to merger through the gain node
	for (let i = 0; i < totalChannels; i++) {
		webAudio.splitter.connect(webAudio.downmixGain, i, 0);
	}

	webAudio.downmixGain.connect(webAudio.merger, 0, 0);

	// Set gain to 1 / sqrt(activeChannels) to maintain perceived loudness
	let gainValue = 1 / Math.sqrt(activeChannels);
	webAudio.downmixGain.gain.setValueAtTime(gainValue, webAudio.audioContext.currentTime);

	log(`Downmixing ${totalChannels} total channels (${activeChannels} active) to mono. Gain set to ${gainValue.toFixed(3)}`);

	return webAudio.merger;
}

function applyLowCut(inputNode, webAudio) {
	// Apply high-pass filter chain for low frequency cut
	webAudio.lowcut1 = webAudio.audioContext.createBiquadFilter();
	webAudio.lowcut1.type = "highpass";
	webAudio.lowcut1.frequency.value = session.lowcut;

	webAudio.lowcut2 = webAudio.audioContext.createBiquadFilter();
	webAudio.lowcut2.type = "highpass";
	webAudio.lowcut2.frequency.value = session.lowcut;

	webAudio.lowcut3 = webAudio.audioContext.createBiquadFilter();
	webAudio.lowcut3.type = "highpass";
	webAudio.lowcut3.frequency.value = session.lowcut;

	inputNode.connect(webAudio.lowcut1);
	webAudio.lowcut1.connect(webAudio.lowcut2);
	webAudio.lowcut2.connect(webAudio.lowcut3);
	
	return webAudio.lowcut3;
}

function applyVoiceChanger(inputNode, webAudio) {
	function makeDistortionCurve(amount = 10) {
		var sampleRate = webAudio.audioContext.sampleRate || 48000;
		var curve = new Float32Array(sampleRate);
		var x;
		for (let i = 0; i < sampleRate; ++i) {
			x = (i * 2) / sampleRate - 1;
			curve[i] = ((3 + amount) * x * 20 * (Math.PI / 180)) / (Math.PI + amount * Math.abs(x));
		}
		return curve;
	}

	let waveShaper = webAudio.audioContext.createWaveShaper();
	waveShaper.curve = makeDistortionCurve(5);

	var realCoeffs = new Float32Array([1, 0]);
	var imagCoeffs = new Float32Array([0, 1]);

	var numCoeffs = 20; // The more coefficients you use, the better the approximation
	var realCoeffs = new Float32Array(numCoeffs);
	var imagCoeffs = new Float32Array(numCoeffs);

	realCoeffs[0] = 0.5;
	for (var i = 1; i < numCoeffs; i++) {
		// note i starts at 1
		imagCoeffs[i] = (1 / (i * Math.PI)) * (1 - Math.random() / 2);
	}

	let oscillator = webAudio.audioContext.createOscillator();
	oscillator.frequency.value = 10;

	const wave = webAudio.audioContext.createPeriodicWave(realCoeffs, imagCoeffs);
	oscillator.setPeriodicWave(wave);

	let oscillatorGain = webAudio.audioContext.createGain();
	oscillatorGain.gain.value = 0.005;
	oscillator.connect(oscillatorGain);
	oscillator.start(0);

	let delay = webAudio.audioContext.createDelay();
	delay.delayTime.value = 0.01;
	oscillatorGain.connect(delay.delayTime);

	let lowEQ = webAudio.audioContext.createBiquadFilter();
	lowEQ.type = "peaking";
	lowEQ.frequency.value = 200;
	lowEQ.Q.value = 0.5;
	lowEQ.gain.value = 6;

	let mid = webAudio.audioContext.createBiquadFilter();
	mid.type = "peaking";
	mid.frequency.value = 500;
	mid.Q.value = 0.5;
	mid.gain.value = -10;
	
	inputNode.connect(delay);
	delay.connect(waveShaper);
	waveShaper.connect(mid);
	mid.connect(lowEQ);
	
	return lowEQ;
}

function applyEqualizer(inputNode, webAudio) {
	// https://webaudioapi.com/samples/frequency-response/ for a tool to help set values
	webAudio.lowEQ = webAudio.audioContext.createBiquadFilter();
	webAudio.lowEQ.type = "lowshelf";
	webAudio.lowEQ.frequency.value = 100;
	webAudio.lowEQ.gain.value = 0;

	webAudio.midEQ = webAudio.audioContext.createBiquadFilter();
	webAudio.midEQ.type = "peaking";
	webAudio.midEQ.frequency.value = 1000;
	webAudio.midEQ.Q.value = 0.5;
	webAudio.midEQ.gain.value = 0;

	webAudio.highEQ = webAudio.audioContext.createBiquadFilter();
	webAudio.highEQ.type = "highshelf";
	webAudio.highEQ.frequency.value = 10000;
	webAudio.highEQ.gain.value = 0;

	inputNode.connect(webAudio.lowEQ);
	webAudio.lowEQ.connect(webAudio.midEQ);
	webAudio.midEQ.connect(webAudio.highEQ);
	
	return webAudio.highEQ;
}

function micDelayNode(mediaStreamSource, audioContext) {
	if (session.micDelay !== false) {
		var delay = parseFloat(session.micDelay / 1000) || 0;
		var delayNode = audioContext.createDelay(delay + 3);
	} else {
		var delay = 0;
		var delayNode = audioContext.createDelay(3);
	}
	delayNode.delayTime.value = delay;
	mediaStreamSource.connect(delayNode);
	return delayNode;
}

function audioGainNode(mediaStreamSource, audioContext) {
	var gainNode = audioContext.createGain();
	if (session.audioGain !== false) {
		var gain = parseFloat(session.audioGain / 100.0) || 0;
	} else {
		var gain = 1.0;
	}
	gainNode.gain.value = gain;
	mediaStreamSource.connect(gainNode);
	return gainNode;
}

function audioGatingNode(mediaStreamSource, audioContext) {
	var gateNode = audioContext.createGain();
	gateNode.gain.value = 1.0;
	mediaStreamSource.connect(gateNode);
	return gateNode;
}

function audioMeter(mediaStreamSource, audioContext) {
	var analyser = audioContext.createAnalyser();
	mediaStreamSource.connect(analyser);
	analyser.fftSize = 256;
	analyser.smoothingTimeConstant = 0.05;

	var bufferLength = analyser.frequencyBinCount;
	var dataArray = new Uint8Array(bufferLength);
	var timer = null;

	var meter1 = document.getElementById("meter1") || false;
	var meter2 = document.getElementById("meter2") || false;
	var meter3 = document.getElementById("meter3") || false;
	var meter4 = document.getElementById("meter4") || false;

	var currentlyActive = 0; // mode 5

	var ng1 = 10;
	var ng2 = 25;
	var ng3 = 30;

	if (session.noisegateSettings) {
		if (session.noisegateSettings.length) {
			ng1 = parseInt(session.noisegateSettings[0]) || 0; // gated volume target (lower to this level)
		}
		if (session.noisegateSettings.length > 1) {
			ng2 = parseInt(session.noisegateSettings[1]) || ng2; // not loud (threshold level)
		}
		if (session.noisegateSettings.length > 2) {
			ng3 = parseInt(session.noisegateSettings[2]) || 0; // stickiness; time (ms)
			ng3 = ng3 / 100.0; // convert to the actual units (100ms)
		}
	}
	if (session.noisegate) {
		changeGatingGain(ng1, 200);
	}

	function draw() {
		try {
			analyser.getByteFrequencyData(dataArray);
			var total = 0;
			for (var i = 0; i < dataArray.length; i++) {
				total += dataArray[i];
			}
			total = total / 100;
			if (session.quietOthers && session.quietOthers == 2) {
				if (total > 10) {
					if (session.muted_activeSpeaker == false) {
						session.muted_activeSpeaker = true;
						session.speakerMuted = true;
						clearTimeout(timer);
						toggleSpeakerMute(true); // okay, sicne this is quietOthers
					}
				} else if (session.muted_activeSpeaker == true) {
					session.speakerMuted = false;
					session.muted_activeSpeaker = false;
					session.activelySpeaking = false;
					clearTimeout(timer);
					timer = setTimeout(function () {
						toggleSpeakerMute(true);
					}, 250); // okay, sicne this is quietOthers
				}
			}

			if (session.pushLoudness == true) {
				var loudnessObj = {};
				loudnessObj[session.streamID] = parseInt(total);
				if (isIFrame) {
					parent.postMessage({ loudness: loudnessObj, action: "loudness", value: total }, session.iframetarget);
				}
			}

			if (session.noisegate) {
				if (total <= ng2) {
					if (currentlyActive == ng3) {
						changeGatingGain(ng1, 200); // set volume to 40% relative to what it is now.
						log("GAIN LOWERED");
						currentlyActive = ng3 + 1;
					} else if (currentlyActive < ng3) {
						currentlyActive += 1;
					}
				} else if (currentlyActive == ng3 + 1) {
					changeGatingGain(100, 200);
					currentlyActive = 0;
					log("GAIN INCREASED");
				} else {
					currentlyActive = 0;
				}
			}

			if (meter1) {
				if (document.getElementById("meter1")) {
					if (total == 0) {
						meter1.style.width = "1px";
						meter2.style.width = "0px";
					} else if (total <= 1) {
						meter1.style.width = "1px";
						meter2.style.width = "0px";
					} else if (total <= 150) {
						meter1.style.width = total + "px";
						meter2.style.width = "0px";
					} else if (total > 150) {
						if (total > 200) {
							total = 200;
						}
						meter1.style.width = "150px";
						meter2.style.width = total - 150 + "px";
					}
				} else {
					meter1 = false;
				}
				if (session.audioGain !== false) {
					if (document.getElementById("previewWebcam")) {
						changeMainGain(100); // full volume while in preview mode
					} else {
						changeMainGain(session.audioGain);
					}
				}
				return;
			} else if (toggleSettingsState && document.getElementById("meter3")) {
				if (total == 0) {
					meter3.style.width = "1px";
					meter4.style.width = "0px";
				} else if (total <= 1) {
					meter3.style.width = "1px";
					meter4.style.width = "0px";
				} else if (total <= 150) {
					meter3.style.width = total + "px";
					meter4.style.width = "0px";
				} else if (total > 150) {
					if (total > 200) {
						meter3.style.width = "150px";
						meter4.style.width = "50px";
					} else {
						meter3.style.width = "150px";
						meter4.style.width = total - 150 + "px";
					}
				}
				if (document.getElementById("mutetoggle")) {
					total *= 3;
					if (total > 255) {
						total = 255;
					}
					total = parseInt(total);
					document.getElementById("mutetoggle").style.color = "rgb(" + (255 - total) + ",255," + (255 - total) + ")";
				}
				meter1 = false;
				return;
			} else if (session.cleanOutput) {
				meter1 = false;
				return;
			} else if (document.getElementById("mutetoggle")) {
				total *= 3;
				if (total > 255) {
					total = 255;
				}
				total = parseInt(total);
				document.getElementById("mutetoggle").style.color = "rgb(" + (255 - total) + ",255," + (255 - total) + ")";
			} else {
				clearInterval(analyser.interval);
				warnlog("METERS  NOT FOUND");
			}
			meter1 = false;
		} catch (e) {
			errorlog(e);
		}
	}

	analyser.interval = setInterval(function () {
		draw();
	}, 100);
	return analyser;
}

function audioCompressor(mediaStreamSource, audioContext) {
	var compressor = audioContext.createDynamicsCompressor();
	compressor.threshold.value = -40;
	compressor.knee.value = 10;
	compressor.ratio.value = 4; // 3
	compressor.attack.value = 0.002; // 0.001
	compressor.release.value = 0.1; // 0.06
	mediaStreamSource.connect(compressor);
	return compressor;
}

function audioLimiter(mediaStreamSource, audioContext) {
	var compressor = audioContext.createDynamicsCompressor();
	compressor.threshold.value = -5;
	compressor.knee.value = 0;
	compressor.ratio.value = 20.0; // 1 to 20
	compressor.attack.value = 0.001;
	compressor.release.value = 0.1;
	mediaStreamSource.connect(compressor);
	return compressor;
}

function activeSpeaker(border = false) {
	var lastActiveSpeaker = null;

	var someoneElseIfSpeaking = false;
	var anyoneIsSpeaking = 0;
	var defaultSpeaker = false;
	var anyVideoAvailable = false; // Track if any video streams are available at all
	var changed = false;
	
	// First pass: check if any video is available
	for (var UUID in session.rpcs) {
		if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && 
			session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) {
			anyVideoAvailable = true;
			break;
		}
	}
	
	for (var UUID in session.rpcs) {
		
		if (session.scene){
			let pass = checkMuteState(UUID);
			// If no one is visible and this person has video, show them immediately
			if (pass && !anyoneIsSpeaking && !defaultSpeaker && anyVideoAvailable === false && 
				session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && 
				session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) {
				session.rpcs[UUID].defaultSpeaker = true;
				defaultSpeaker = true;
				anyVideoAvailable = true;
				changed = true;
				continue;
			} else if (pass){
				session.rpcs[UUID].activelySpeaking = false;
				if (session.rpcs[UUID].defaultSpeaker && session.rpcs[UUID].defaultSpeaker !== true) {
					clearTimeout(session.rpcs[UUID].defaultSpeaker);
				}
				session.rpcs[UUID].defaultSpeaker = false;
				continue;
			}
		}
		
		if (session.activeSpeaker > 2 && !(session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted)) {
			session.rpcs[UUID].activelySpeaking = false; // we're not showing audio-only sources in this mode.
			if (session.rpcs[UUID].defaultSpeaker && session.rpcs[UUID].defaultSpeaker !== true) {
				clearTimeout(session.rpcs[UUID].defaultSpeaker);
			}
			session.rpcs[UUID].defaultSpeaker = false;
			continue;
		}

		if (session.rpcs[UUID].stats._Audio_Loudness_average) {
			if (session.rpcs[UUID].stats.Audio_Loudness && session.rpcs[UUID].stats.Audio_Loudness > 10) {
				session.rpcs[UUID].stats._Audio_Loudness_average = parseFloat(session.rpcs[UUID].stats.Audio_Loudness * 0.07 + session.rpcs[UUID].stats._Audio_Loudness_average * 0.93);
			} else {
				session.rpcs[UUID].stats._Audio_Loudness_average = parseFloat(session.rpcs[UUID].stats._Audio_Loudness_average * 0.975);
			}
		} else {
			session.rpcs[UUID].stats._Audio_Loudness_average = 1;
		}
		if (session.rpcs[UUID].stats._Audio_Loudness_average > 13) {
			if (border) {
				if (session.rpcs[UUID].videoElement) {
					session.rpcs[UUID].videoElement.style.border = "green solid 1px";
					session.rpcs[UUID].videoElement.style.padding = "0";
				}
			} else if (!session.rpcs[UUID].activelySpeaking) {
				session.rpcs[UUID].activelySpeaking = true;
				lastActiveSpeaker = UUID;
				session.rpcs[UUID].stats._Audio_Loudness_average += 50;
			}
		} else if (session.rpcs[UUID].stats._Audio_Loudness_average > 6) {
			//
		} else {
			if (border) {
				if (session.rpcs[UUID].videoElement) {
					session.rpcs[UUID].videoElement.style.border = "";
					session.rpcs[UUID].videoElement.style.padding = "1px";
				}
			} else if (session.rpcs[UUID].activelySpeaking) {
				session.rpcs[UUID].activelySpeaking = false;
				lastActiveSpeaker = UUID;
			}
		}
		if (session.rpcs[UUID].stats.Audio_Loudness > 13 || (session.rpcs[UUID].stats.Audio_Loudness > 5 && session.rpcs[UUID].stats._Audio_Loudness_average > 3) || session.rpcs[UUID].stats._Audio_Loudness_average > 6) {
			someoneElseIfSpeaking = true;
		}
		if (session.rpcs[UUID].activelySpeaking) {
			anyoneIsSpeaking += 1;
		}
		if (session.rpcs[UUID].defaultSpeaker === true) {
			defaultSpeaker = true;
		}
	}

	var loudest = null;
	var loudestActive = null;
	
	if (session.activeSpeaker === 1 || session.activeSpeaker === 3) {
		// will only show one speaker at a time; the loudest or last-loud speaker
		if (!anyoneIsSpeaking) {
			if (defaultSpeaker) {
				// already good to go.
			} else if (lastActiveSpeaker) {
				if (session.rpcs[lastActiveSpeaker].defaultSpeaker !== false) {
					clearTimeout(session.rpcs[lastActiveSpeaker].defaultSpeaker);
				} else {
					changed = true;
					log("lastActiveSpeaker is default");
				}
				session.rpcs[lastActiveSpeaker].defaultSpeaker = true;
			} else if (session.scene === false || (session.nopreview === false && session.minipreview !== 1)) {
				// we don't need to care.
			} else if (anyVideoAvailable === false) {
				// Immediately select the first available video source if no one is currently visible
				for (var UUID in session.rpcs) {
					if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && 
						session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) {
						
						if (session.rpcs[UUID].defaultSpeaker !== false) {
							clearTimeout(session.rpcs[UUID].defaultSpeaker);
						} else {
							changed = true;
							log(UUID + " is speaker now (no lull)");
						}
						session.rpcs[UUID].defaultSpeaker = true;
						break;
					}
				}
				// Fall through to original logic if needed
				if (!changed) {
					for (var UUID in session.rpcs) {
						if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) {
							if (session.rpcs[UUID].defaultSpeaker !== false) {
								clearTimeout(session.rpcs[UUID].defaultSpeaker);
							} else {
								changed = true;
								log(UUID + " is speaker now");
							}
							session.rpcs[UUID].defaultSpeaker = true;
							break;
						}
					}
					if (!changed && session.activeSpeaker <= 2) {
						// switch to streams that have no video track
						for (var UUID in session.rpcs) {
							if (session.rpcs[UUID].label) {
								if (session.rpcs[UUID].defaultSpeaker !== false) {
									clearTimeout(session.rpcs[UUID].defaultSpeaker);
								} else {
									changed = true;
									log(UUID + " is speaker now");
								}
								session.rpcs[UUID].defaultSpeaker = true;
								break;
							} else if (!changed) {
								if (session.rpcs[UUID].defaultSpeaker !== false) {
									clearTimeout(session.rpcs[UUID].defaultSpeaker);
								} else {
									changed = true;
									log(UUID + " is speaker now");
								}
								session.rpcs[UUID].defaultSpeaker = true;
							}
						}
					}
				}
			} else {
				for (var UUID in session.rpcs) {
					if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) {
						if (session.rpcs[UUID].defaultSpeaker !== false) {
							clearTimeout(session.rpcs[UUID].defaultSpeaker);
						} else {
							changed = true;
							log(UUID + " is speaker now");
						}
						session.rpcs[UUID].defaultSpeaker = true;
						break;
					}
				}
				if (!changed && session.activeSpeaker <= 2) {
					// switch to streams that have no video track
					for (var UUID in session.rpcs) {
						if (session.rpcs[UUID].label) {
							if (session.rpcs[UUID].defaultSpeaker !== false) {
								clearTimeout(session.rpcs[UUID].defaultSpeaker);
							} else {
								changed = true;
								log(UUID + " is speaker now");
							}
							session.rpcs[UUID].defaultSpeaker = true;
							break;
						} else if (!changed) {
							if (session.rpcs[UUID].defaultSpeaker !== false) {
								clearTimeout(session.rpcs[UUID].defaultSpeaker);
							} else {
								changed = true;
								log(UUID + " is speaker now");
							}
							session.rpcs[UUID].defaultSpeaker = true;
						}
					}
				}
			}
		} else {
			for (var UUID in session.rpcs) {
				if (!("_Audio_Loudness_average" in session.rpcs[UUID].stats)) {
					// never could have been loudest, since no loudness value.
					continue;
				}

				if (session.rpcs[UUID].activelySpeaking) {
					if (!loudestActive) {
						loudestActive = UUID;
					} else if (session.rpcs[UUID].stats._Audio_Loudness_average > session.rpcs[loudestActive].stats._Audio_Loudness_average) {
						if (session.rpcs[loudestActive].defaultSpeaker === true) {
							if (!session.activeSpeakerTimeout) {
								session.rpcs[loudestActive].defaultSpeaker = false;
								changed = true;
								log(loudestActive + " is loudest but not speaker anymore");
							} else {
								session.rpcs[loudestActive].defaultSpeaker = setTimeout(
									function (uuid) {
										session.rpcs[uuid].defaultSpeaker = false;
										updateMixer();
									},
									session.activeSpeakerTimeout,
									loudestActive
								);
							}
						}
						loudestActive = UUID;
					} else if (session.rpcs[UUID].defaultSpeaker === true) {
						if (!session.activeSpeakerTimeout) {
							session.rpcs[UUID].defaultSpeaker = false;
							changed = true;
							log(UUID + " is not speaker anymore");
						} else {
							session.rpcs[UUID].defaultSpeaker = setTimeout(
								function (uuid) {
									session.rpcs[uuid].defaultSpeaker = false;
									updateMixer();
								},
								session.activeSpeakerTimeout,
								UUID
							);
						}
					}
				} else if (session.rpcs[UUID].defaultSpeaker === true) {
					if (!session.activeSpeakerTimeout) {
						session.rpcs[UUID].defaultSpeaker = false;
						changed = true;
						log(UUID + " is not speaker anymore");
					} else {
						session.rpcs[UUID].defaultSpeaker = setTimeout(
							function (uuid) {
								session.rpcs[uuid].defaultSpeaker = false;
								updateMixer();
							},
							session.activeSpeakerTimeout,
							UUID
						);
					}
				}
			}

			if (loudestActive && session.rpcs[loudestActive].defaultSpeaker !== true) {
				if (session.rpcs[loudestActive].defaultSpeaker) {
					clearTimeout(session.rpcs[loudestActive].defaultSpeaker);
				} else {
					changed = true;
				}
				for (let UUID in session.rpcs) {
					if (loudestActive !== UUID) {
						if (session.rpcs[UUID].defaultSpeaker === true) {
							session.rpcs[UUID].defaultSpeaker = false; // Reset immediately before any new logic
							changed = true;
						} else if (session.rpcs[UUID].defaultSpeaker) {
							clearTimeout(session.rpcs[UUID].defaultSpeaker);
							session.rpcs[UUID].defaultSpeaker = false;
						}
					}
				}

				session.rpcs[loudestActive].defaultSpeaker = true;
			}
		}
	} else if (session.activeSpeaker === 2 || session.activeSpeaker === 4) {
		// will show whoever is talking; mixed together; if no one is talking, just shows yourself

		if (!anyoneIsSpeaking) {
			if (defaultSpeaker) {
				// already good to go.
			} else if (lastActiveSpeaker) {
				if (session.rpcs[lastActiveSpeaker].defaultSpeaker !== false) {
					clearTimeout(session.rpcs[lastActiveSpeaker].defaultSpeaker);
				} else {
					changed = true;
				}
				session.rpcs[lastActiveSpeaker].defaultSpeaker = true;
			} else if (session.scene === false || (session.nopreview === false && session.minipreview !== 1)) {
				// we don't need to care.
			} else if (anyVideoAvailable === false) {
				// Immediately select the first available video source if no one is currently visible
				for (var UUID in session.rpcs) {
					if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && 
						session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) {
						
						if (session.rpcs[UUID].defaultSpeaker !== false) {
							clearTimeout(session.rpcs[UUID].defaultSpeaker);
						} else {
							changed = true;
							log(UUID + " is speaker now (no lull)");
						}
						session.rpcs[UUID].defaultSpeaker = true;
						break;
					}
				}
				// Fall through to original logic if needed
				if (!changed) {
					for (var UUID in session.rpcs) {
						if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) {
							if (session.rpcs[UUID].defaultSpeaker !== false) {
								clearTimeout(session.rpcs[UUID].defaultSpeaker);
							} else {
								changed = true;
							}
							session.rpcs[UUID].defaultSpeaker = true;
							break;
						}
					}
					if (!changed && session.activeSpeaker <= 2) {
						for (var UUID in session.rpcs) {
							if (session.rpcs[UUID].label) {
								if (session.rpcs[UUID].defaultSpeaker !== false) {
									clearTimeout(session.rpcs[UUID].defaultSpeaker);
								} else {
									changed = true;
								}
								session.rpcs[UUID].defaultSpeaker = true;
								break;
							} else if (!changed) {
								if (session.rpcs[UUID].defaultSpeaker !== false) {
									clearTimeout(session.rpcs[UUID].defaultSpeaker);
								} else {
									changed = true;
								}
								session.rpcs[UUID].defaultSpeaker = true;
							}
						}
					}
				}
			} else {
				for (var UUID in session.rpcs) {
					if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) {
						if (session.rpcs[UUID].defaultSpeaker !== false) {
							clearTimeout(session.rpcs[UUID].defaultSpeaker);
						} else {
							changed = true;
						}
						session.rpcs[UUID].defaultSpeaker = true;
						break;
					}
				}
				if (!changed && session.activeSpeaker <= 2) {
					for (var UUID in session.rpcs) {
						if (session.rpcs[UUID].label) {
							if (session.rpcs[UUID].defaultSpeaker !== false) {
								clearTimeout(session.rpcs[UUID].defaultSpeaker);
							} else {
								changed = true;
							}
							session.rpcs[UUID].defaultSpeaker = true;
							break;
						} else if (!changed) {
							if (session.rpcs[UUID].defaultSpeaker !== false) {
								clearTimeout(session.rpcs[UUID].defaultSpeaker);
							} else {
								changed = true;
							}
							session.rpcs[UUID].defaultSpeaker = true;
						}
					}
				}
			}
		} else {
			for (var UUID in session.rpcs) {
				if (session.rpcs[UUID].activelySpeaking && !session.rpcs[UUID].defaultSpeaker) {
					session.rpcs[UUID].defaultSpeaker = true;
					changed = true;
				} else if (!session.rpcs[UUID].activelySpeaking && session.rpcs[UUID].defaultSpeaker) {
					if (!session.activeSpeakerTimeout) {
						session.rpcs[UUID].defaultSpeaker = false;
						changed = true;
					} else if (session.rpcs[UUID].defaultSpeaker === true) {
						session.rpcs[UUID].defaultSpeaker = setTimeout(
							function (uuid) {
								session.rpcs[uuid].defaultSpeaker = false;
								updateMixer();
							},
							session.activeSpeakerTimeout,
							UUID
						);
					}
				}
			}
		}
	}
	if (session.quietOthers && session.quietOthers === 1) {
		if (someoneElseIfSpeaking) {
			if (session.muted_activeSpeaker == false) {
				session.muted_activeSpeaker = true;
				session.muted = true;
				toggleMute(true);
			}
		} else if (session.muted_activeSpeaker == true) {
			session.muted = false;
			session.muted_activeSpeaker = false;
			toggleMute(true);
		}
	} else if (session.quietOthers && session.quietOthers === 3) {
		// purely for fun. It's the opposite of a noise-gate I guess.
		if (someoneElseIfSpeaking) {
			if (session.muted_activeSpeaker == false) {
				session.muted_activeSpeaker = true;
				session.speakerMuted = true;
				toggleSpeakerMute(true); // okay, sicne this is quietOthers
			}
		} else if (session.muted_activeSpeaker == true) {
			session.speakerMuted = false;
			session.muted_activeSpeaker = false;
			toggleSpeakerMute(true); // okay, sicne this is quietOthers
		}
	}

	if (changed) {
		setTimeout(function () {
			updateMixer();
		}, 0);
	}
}

function randomizeArray(unshuffled) {
	var arr = unshuffled
		.map(a => ({
			sort: Math.random(),
			value: a
		}))
		.sort((a, b) => a.sort - b.sort)
		.map(a => a.value); // shuffle once

	for (var i = arr.length - 1; i > 0; i--) {
		// shuffle twice
		var j = Math.floor(Math.random() * (i + 1));
		var tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}
	return arr;
}

function joinRoom(roomname) {
	if (roomname.length) {
		roomname = sanitizeRoomName(roomname);
		log("Join room: " + roomname);
		updateVolume(false); // chance of a race condition, but unlikely and not a big deal if so.
		session.joinRoom(roomname).then(
			function (response) {
				// callback from server; we've joined the room. Just the listing is returned

				if (session.joiningRoom === "seedPlz") {
					// allow us to seed, now that we have joined the room.
					session.joiningRoom = false; // joined
					session.seedStream();
				} else {
					session.joiningRoom = false; // no seeding callback
				}
				var token = "";
				if (session.token) {
					token += "&token=" + session.token;
				}

				if (!session.cleanOutput) {
					if (session.roomhost) {
						if (session.defaultPassword === false) {
							if (session.password === false) {
								var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + "&password=false" + token;
								warnUser("You can invite others with:\n\n<a target='_blank' title='Copy this link to the clipboard' style='cursor:pointer' onclick='copyFunction(this.innerText,event);' href='" + invite + "'>" + invite + "</a>", false, false);
							} else {
								generateHash(session.password + session.salt, 4).then(function (hash) {
									// change the hash length from 4 to 3 when VDO.Ninja v24.10 or newer is in production.
									var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + "&hash=" + hash + token;
									warnUser("You can invite others with:\n\n<a target='_blank' title='Copy this link to the clipboard' style='cursor:pointer' onclick='copyFunction(this.innerText,event)' href='" + invite + "'>" + invite + "</a>", false, false);
								});
							}
						} else {
							var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + token;
							warnUser("You can invite others with:\n\n<a target='_blank' title='Copy this link to the clipboard' style='cursor:pointer' onclick='copyFunction(this.innerText,event)' href='" + invite + "'>" + invite + "</a>", false, false);
						}
					}
				}

				log("Members in Room");
				log(response);

				if (session.randomize === true) {
					response = randomizeArray(response);
					log("Randomized List of Viewers");
					log(response);
					for (var i in response) {
						if ("UUID" in response[i]) {
							if (response[i].streamID) {
								if (response[i].UUID in session.rpcs) {
									log("RTC already connected"); /// lets just say instead of Stream, we have
								} else {
									log(response[i].streamID);
									var streamID = session.desaltStreamID(response[i].streamID);
									if (session.queue) {
										if (session.directorList.indexOf(response[i].UUID) >= 0) {
											if (session.queueType == 2) {
												warnlog("PLAYING DIRECTOR");
												play(streamID, response[i].UUID);
											}
										} else if (session.view_set && session.view_set.includes(streamID)) {
											play(streamID, response[i].UUID);
										} else if (session.queueList.length < 5000) {
											if (!(streamID in session.watchTimeoutList) && !session.queueList.includes(streamID)) {
												session.queueList.push(streamID);
											}
										}
									} else {
										log("STREAM ID DESALTED 3: " + streamID);
										setTimeout(
											function (sid) {
												play(sid);
											},
											Math.floor(Math.random() * 100),
											streamID
										); // add some furtherchance with up to 100ms added latency
									}
								}
							}
						}
					}
				} else {
					for (var i in response) {
						if ("UUID" in response[i]) {
							if (response[i].streamID) {
								if (response[i].UUID in session.rpcs) {
									log("RTC already connected"); /// lets just say instead of Stream, we have
								} else {
									log(response[i].streamID);
									var streamID = session.desaltStreamID(response[i].streamID);
									if (session.queue) {
										if (session.directorList.indexOf(response[i].UUID) >= 0) {
											if (session.queueType == 2) {
												play(streamID, response[i].UUID);
											}
										} else if (session.view_set && session.view_set.includes(streamID)) {
											play(streamID, response[i].UUID);
										} else if (session.queueList.length < 5000) {
											if (!(streamID in session.watchTimeoutList) && !session.queueList.includes(streamID)) {
												session.queueList.push(streamID);
											}
										}
									} else {
										log("STREAM ID DESALTED 4: " + streamID);
										play(streamID, response[i].UUID); // play handles the group room mechanics here
									}
								}
							}
						}
					}
				}
				updateQueue();
				pokeIframeAPI("joined-room-complete");

				if (session.include.length) {
					// we want to request what hasn't been requested already, since we are joining a room.
					session.include.forEach(sid => {
						if (sid in session.waitingWatchList) {
							return;
						} else {
							session.watchStream(sid);
						}
					});
				}
			},
			function (error) {
				return {};
			}
		);
	} else {
		log("Room name not long enough or contained all bad characaters");
	}
}

async function createRoom(roomname = false, reload = false) {
	if (reload === true) {
		let oldDirectorSettings = getStorage("directorOtherSettings");
		var passwordRoom = oldDirectorSettings.password;
		if (passwordRoom === session.defaultPassword) {
			passwordRoom = "";
		} else if (passwordRoom === false) {
			passwordRoom = "";
			session.password = false;
		}
		roomname = oldDirectorSettings.roomid;
		if (!roomname) {
			warnUser("Couldn't load previous session");
			return;
		}
		if (urlParams.has("dir")) {
			updateURL("dir=" + roomname, true, false); // make the link reloadable.
		} else {
			updateURL("director=" + roomname, true, false); // make the link reloadable.
		}

		session.codecGroupFlag = session.codecGroupFlag || oldDirectorSettings.codecGroupFlag || session.codecGroupFlag;

		session.label = session.label || oldDirectorSettings.label || session.label;
		session.codecGroupFlag = session.codecGroupFlag || oldDirectorSettings.codecGroupFlag || session.codecGroupFlag;
		session.showDirector = session.showDirector || oldDirectorSettings.showDirector || session.showDirector;

		if (oldDirectorSettings.broadcast) {
			getById("broadcastFlag").checked = true;
		}
		if (session.showDirector) {
			getById("showdirectorFlag").checked = true;
		}
	} else {
		if (roomname == false) {
			roomname = getById("videoname1").value;
			roomname = sanitizeRoomName(roomname);

			clearDirectorSettings();

			if (roomname.length != 0) {
				if (urlParams.has("dir")) {
					updateURL("dir=" + roomname, true, false); // make the link reloadable.
				} else {
					updateURL("director=" + roomname, true, false); // make the link reloadable.
				}
			}
		}
		if (roomname.length == 0) {
			//if (!(session.cleanOutput)) {
			//	warnUser("Please enter a room name before continuing");
			//}

			getById("videoname1").focus();
			getById("videoname1").classList.remove("shake");
			setTimeout(function () {
				getById("videoname1").classList.add("shake");
			}, 10);

			return;
		}
		log(roomname);

		var passwordRoom = document.getElementById("passwordRoom") ? sanitizePassword(document.getElementById("passwordRoom").value) : "";
	}

	session.roomid = roomname;
	getById("dirroomid").innerHTML = decodeURIComponent(session.roomid);
	getById("roomid").innerHTML = session.roomid;

	var passAdd = "";
	var passAdd2 = "";

	if (passwordRoom.length) {
		session.password = passwordRoom;
		session.defaultPassword = false;

		if (session.password === "false" || session.password === "0" || session.password === "off") {
			session.password = false;
			if (urlParams.has("pass")) {
				updateURL("pass=0");
				passAdd = "&pass=0";
				passAdd2 = "&pass=0";
			} else if (urlParams.has("pw")) {
				updateURL("pw=0");
				passAdd = "&pw=0";
				passAdd2 = "&pw=0";
			} else if (urlParams.has("p")) {
				updateURL("p=0");
				passAdd = "&p=0";
				passAdd2 = "&p=0";
			} else if (urlParams.has("password")) {
				updateURL("password=false");
				passAdd = "&password=false";
				passAdd2 = "&password=false";
			} else {
				updateURL("p=0");
				passAdd = "&p=0";
				passAdd2 = "&p=0";
			}
		} else {
			if (urlParams.has("pass")) {
				updateURL("pass=" + session.password);
			} else if (urlParams.has("pw")) {
				updateURL("pw=" + session.password);
			} else if (urlParams.has("p")) {
				updateURL("p=" + session.password);
			} else {
				updateURL("password=" + session.password);
			}
		}
	}

	await registerToken();

	if (session.defaultPassword === false && session.password) {
		passAdd2 = "&password=" + session.password;
		return generateHash(session.password + session.salt, 4)
			.then(async function (hash) {
				passAdd = "&hash=" + hash;
				await createRoomCallback(passAdd, passAdd2);
			})
			.catch(errorlog);
	} else if (session.defaultPassword === false && session.password === false) {
		passAdd = "&p=0";
		passAdd2 = "&p=0";
		await createRoomCallback(passAdd, passAdd2);
	} else {
		await createRoomCallback(passAdd, passAdd2);
	}
}

function copyVideoFrameToClipboard(videoElement, e = false) {
	try {
		var canvas = document.createElement("canvas");

		canvas.width = videoElement.videoWidth;
		canvas.height = videoElement.videoHeight;

		var ctx = canvas.getContext("2d");
		ctx.drawImage(videoElement, 0, 0);

		var img = new Image();
		img.src = canvas.toDataURL();

		canvas.toBlob(function (blob) {
			navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
		}, "image/png");

		popupMessage(e, "Frame copied to clipboard as as PNG Image");
	} catch (e) {
		errorlog(e);
	}
}

function saveVideoFrameToDisk(videoElement, e = false, filename = false) {
	try {
		var canvas = document.createElement("canvas");

		canvas.width = videoElement.videoWidth;
		canvas.height = videoElement.videoHeight;

		var ctx = canvas.getContext("2d");
		ctx.drawImage(videoElement, 0, 0);

		var img = new Image();
		img.src = canvas.toDataURL();

		canvas.toBlob(function (blob) {
			var link = document.createElement("a");
			if (filename) {
				link.download = filename;
			} else if (e) {
				link.download = (videoElement.id || "video") + "_" + parseInt(performance.now()) + ".png";
			} else {
				link.download = (videoElement.id || "video") + "_" + parseInt(Date.now()) + ".png";
			}
			link.href = URL.createObjectURL(blob);
			link.click();
			URL.revokeObjectURL(link.href);
		}, "image/png");
		if (e) {
			popupMessage(e, "Saving current frame to disk");
		}
	} catch (e) {
		errorlog(e);
	}
}

function sendVideoFrameToIframe(videoElement, e = false, request = {}) {
	try {
		var canvas = document.createElement("canvas");

		canvas.width = videoElement.videoWidth;
		canvas.height = videoElement.videoHeight;

		var ctx = canvas.getContext("2d");
		ctx.drawImage(videoElement, 0, 0);

		var img = new Image();
		img.src = canvas.toDataURL();

		canvas.toBlob(function (blob) {
			var response = {};
			response.imageData = blob;
			response.imageType = "png";
			if (request.streamID) {
				response.streamID = request.streamID;
			}
			if (request.UUID) {
				response.UUID = request.UUID;
			}
			if (request.cib) {
				response.cib = request.cib;
			}
			if (videoElement.id) {
				response.videoID = videoElement.id;
			}

			pokeIframeAPI("image-frame-capture", response);
		}, "image/png");
	} catch (e) {
		errorlog(e);
	}
}

async function checkDirectorStreamID() {
	if (session.directorStreamID) {
		for (var UUID in session.rpcs) {
			if (session.rpcs[UUID].streamID) {
				var hashedSID = await generateHash(session.rpcs[UUID].streamID);
				if (hashedSID === session.directorStreamID) {
					session.directorUUID = UUID; // main director
					session.directorList = [];
					session.directorList.push(UUID); // approved co/directors
					session.directorUUID = UUID;
					session.newMainDirectorSetup();
					return;
				}
			}
		}
		for (var UUID in session.pcs) {
			if (session.pcs[UUID].streamID) {
				var hashedSID = await generateHash(session.pcs[UUID].streamID);
				if (hashedSID === session.directorStreamID) {
					session.directorList = [];
					session.directorList.push(UUID);
					session.directorUUID = UUID;
					session.newMainDirectorSetup();
					return;
				}
			}
		}
		if (session.streamID == session.directorStreamID) {
			session.directorState = true;
			session.directorUUID = false;
			pokeAPI("director", true);
			pokeIframeAPI("director", true);
			warnlog("You are joining with a token, but are the director?");
		}
		session.directorList = [];
	}
}

async function checkToken() {
	// this lets us use a server+password validation method for the director.
	if (!session.token) {
		return;
	}
	if (!session.roomid) {
		return;
	}
	if (session.mainDirectorPassword) {
		return;
	}

	try {
		var request = new XMLHttpRequest();

		var hashedRoom = session.roomid;
		if (session.password) {
			hashedRoom += session.password;
		}
		hashedRoom += "i^4&u#Fz5Eu#MsK^chF5*XAEYi1g";
		hashedRoom = await generateHash(hashedRoom);
		hashedRoom = hashedRoom.slice(0, 50);

		request.open("GET", "https://tokens.vdo.ninja/?token=" + session.token + "&room=" + hashedRoom, false);
		request.send(null);

		if (request.status === 200) {
			try {
				var result = JSON.parse(request.responseText);
				if ("UUID" in result) {
					session.directorUUID = result.UUID;
					session.directorList = [];
					session.directorList.push(session.directorUUID);
					session.directorStreamID = false;
					session.newMainDirectorSetup();
				} else if ("streamID" in result) {
					session.directorStreamID = result.streamID;
					checkDirectorStreamID();
				}
			} catch (e) {
				session.directorUUID = false;
				session.directorStreamID = false;
				session.directorList = [];
				errorlog(e);
			}
		} else {
			session.directorUUID = false;
			session.directorStreamID = false;
			session.directorList = [];
			errorlog("Didn't get a token response");
		}
	} catch (e) {
		errorlog(e);
	}
}

async function registerToken() {
	// this lets us use a server+password validation method for the director.
	if (!session.roomid) {
		return;
	}
	if (!session.streamID) {
		return;
	}
	if (!session.mainDirectorPassword) {
		return;
	}

	var longToken = session.mainDirectorPassword + "3wJVW^5qYU4DxGi6VhxN6RF04Q%$"; // this lets us use the same token across multiple rooms
	var hashedToken = await generateHash(longToken); // keep it anonymous
	hashedToken = hashedToken.slice(0, 50);

	var hashedRoom = session.roomid;
	if (session.password) {
		hashedRoom += session.password;
	}
	hashedRoom += "i^4&u#Fz5Eu#MsK^chF5*XAEYi1g";
	hashedRoom = await generateHash(hashedRoom);
	hashedRoom = hashedRoom.slice(0, 50);

	var data2send = {};
	var hashedSID = await generateHash(session.streamID);
	data2send.streamID = hashedSID; // not sure if there's a way around this.
	data2send = JSON.stringify(data2send);

	var request = new XMLHttpRequest();
	request.open("POST", "https://tokens.vdo.ninja/?token=" + hashedToken + "&room=" + hashedRoom, false);
	console.log("https://tokens.vdo.ninja/?token=" + hashedToken + "&room=" + hashedRoom);
	request.send(data2send);

	if (request.status === 200) {
		try {
			if (request.responseText && request.responseText.length === 16) {
				session.token = request.responseText;
				console.log("share token: " + session.token);
				session.directorState = true;
				pokeAPI("director", true);
				pokeIframeAPI("director", true);
			}
		} catch (e) {
			session.directorState = false;
			pokeAPI("director", false);
			pokeIframeAPI("director", false);
		}
	} else {
		session.directorState = false;
		pokeAPI("director", false);
		pokeIframeAPI("director", false);
	}
}

function hideDirectorinvites(ele, skip = true) {
	if (getById("directorLinks2").style.display == "none") {
		ele.innerHTML = '<i class="las la-caret-down"></i><span data-translate="hide-the-links"> LINKS (GUEST INVITES & SCENES)</span>';
		getById("directorLinks2").style.display = "inline-block";
		getById("customizeLinks").classList.remove("hidden");
	} else {
		ele.innerHTML = '<i class="las la-caret-right"></i><span data-translate="hide-the-links"> LINKS (GUEST INVITES & SCENES)</span>';
		getById("directorLinks2").style.display = "none";
		getById("help_directors_room").style.display = "none";
		getById("roomnotes2").style.display = "none";
		getById("customizeLinks").classList.add("hidden");
	}
	if (getById("directorLinks1").style.display == "none") {
		getById("directorLinks1").style.display = "inline-block";
		getById("customizeLinks").classList.remove("hidden");
	} else {
		getById("directorLinks1").style.display = "none";
		getById("help_directors_room").style.display = "none";
		getById("roomnotes2").style.display = "none";
		getById("customizeLinks").classList.add("hidden");
	}
	if (skip) {
		saveDirectorSettings();
	}
}

function toggleCoDirector_changeurl(ele) {
	session.codirector_changeURL = ele.checked; // doesn't do anything yet though.
}

function toggleCoDirector_transfer(ele) {
	session.codirector_transfer = ele.checked;
}

async function toggleCoDirector(ele) {
	//session.coDirectorAllowed = ele.checked;
	if (!ele.checked) {
		getById("codirectorSettings").style.display = "none";
		return;
	}
	if (!session.directorPassword) {
		session.directorPassword = await promptAlt(getTranslation("enter-new-codirector-password"), false);
		if (!session.directorPassword) {
			session.directorPassword = false;
			ele.checked = false;
			return;
		}
		session.directorPassword = sanitizePassword(session.directorPassword);
	}
	updateURL("codirector=" + session.directorPassword, true, false);
	getById("coDirectorEnableSpan").style.display = "none";

	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);

	if (session.codirector_transfer) {
		getById("codirectorSettings_transfer").checked = true;
	} else {
		getById("codirectorSettings_transfer").checked = false;
	}
	if (session.codirector_changeURL) {
		getById("codirectorSettings_changeurl").checked = true;
	} else {
		getById(codirectorSettings_changeurl).checked = false;
	}

	var token = "";
	if (session.token) {
		token += "&token=" + session.token;
	}

	getById("codirectorSettings_invite").value = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token;
	if (session.password !== session.sitePassword) {
		if (session.password === false) {
			getById("codirectorSettings_invite").value += "&password=false";
		} else {
			getById("codirectorSettings_invite").value += "&password=" + session.password;
		}
	}

	getById("codirectorSettings").style.display = "block";
}

function getParentHostname() {
  const parentUrl = document.referrer;
  if (parentUrl) {
    const url = new URL(parentUrl);
    return url.hostname;
  }
  return null;
}

async function toggleWidgetURL(ele) {
	if (ele.id === "widgetURL") {
		ele = getById("widgetURCheck");
	} else if (!ele.checked) {
		getById("widgetURL").classList.add("hidden");
		session.widget = false;

		var data = {};
		data.widgetSrc = false;
		for (var UUID in session.pcs) {
			if (session.pcs[UUID].allowWidget === true) {
				session.sendMessage(data, UUID);
			}
		}

		if (session.director) {
			let widget = document.getElementById("widget");
			if (widget) {
				getById("widget").remove();
				getById("directorlayout").classList.remove("widget");
				getById("directorlayout").classList.remove("left");
			}
		}
		pokeIframeAPI("widget-src", session.widget);
		return;
	}
	var widget = await promptAlt(getTranslation("enter-url-for-widget"), false, false, session.widget);
	if (widget !== null) {
		session.widget = widget;
	}
	if (session.widget) {
		getById("widgetURL").value = session.widget;
		getById("widgetURL").classList.remove("hidden");
		updateMixer();
	} else {
		session.widget = false;
		getById("widgetURL").classList.add("hidden");
		ele.checked = false;
	}

	var data = {};
	data.widgetSrc = session.widget;
	for (var UUID in session.pcs) {
		if (session.pcs[UUID].allowWidget === true) {
			session.sendMessage(data, UUID);
		}
	}

	if (session.director) {
		let widget = document.getElementById("widget");
		if (!widget && session.widget && session.iFramesAllowed) {
				widget = document.createElement("iframe");
				widget.id = "widget";
				widget = loadIframe(parseURL4Iframe(session.widget), widget);
				if (widget){
					getById("directorlayout").classList.add("widget");
					
					if (session.widgetleft){
						widget.classList.add("left");
						getById("directorlayout").classList.add("left");
					}
					
					log(widget.src);
					document.body.appendChild(widget);
				}
		} else if (session.widget && widget && session.iFramesAllowed) {
			loadIframe(parseURL4Iframe(session.widget), widget);
		} else if (widget){
			getById("widget").remove();
			getById("directorlayout").classList.remove("widget");
			getById("directorlayout").classList.remove("left");
		}
	}

	pokeIframeAPI("widget-src", session.widget);
}

async function createRoomCallback(passAdd, passAdd2) {
	if (session.meshcast) {
		if (!session.cleanOutput && !session.cleanDirector) {
			document.getElementById("meshcastMenu").classList.remove("hidden");
		}
	}

	if (!session.switchMode) {
		getById("directorlayout").classList.remove("hidden");
		getById("gridlayout").classList.add("hidden");
	}

	var broadcastFlag = getById("broadcastFlag");
	try {
		if (broadcastFlag.checked) {
			broadcastFlag = true;
		} else {
			broadcastFlag = false;
		}
	} catch (e) {
		broadcastFlag = false;
	}

	var broadcastString = "";
	if (broadcastFlag) {
		broadcastString = "&broadcast";
		getById("broadcastSlider").checked = true;
		//customizeLinks1
		//saveDirectorSettings
	}

	var wss = "";
	if (session.wssSetViaUrl) {
		if (session.customWSS && session.customWSS !== true) {
			wss = "&pie=" + session.customWSS;
		} else if (session.customWSS==true){
			wss = "&wss=" + session.wss;
		} else {
			wss = "&wss2=" + session.wss;
		}
	}

	var queue = "";
	if (session.queue) {
		queue = "&queue";
		getById("directorLinks2").style.opacity = "0.2";
		getById("directorLinks2").style.pointerEvents = "none";
		getById("directorLinks2").style.cursor = "not-allowed";
	}

	var showdirectorFlag = getById("showdirectorFlag");
	try {
		if (showdirectorFlag.checked) {
			showdirectorFlag = true;
		} else {
			showdirectorFlag = false;
		}
	} catch (e) {
		showdirectorFlag = false;
	}

	if (showdirectorFlag) {
		updateURL("showdirector", true, false);
		session.showDirector = session.showDirector || true;
		//getById("broadcastSlider").checked=true;
	}

	var codecGroupFlag = getById("codecGroupFlag");

	if (session.codecGroupFlag) {
		codecGroupFlag = session.codecGroupFlag || "";
	} else if (codecGroupFlag) {
		if (codecGroupFlag.value) {
			if (codecGroupFlag.value === "vp9") {
				codecGroupFlag = "&codec=vp9";
				getById("codech264toggle").disabled = true;
			} else if (codecGroupFlag.value === "h264") {
				codecGroupFlag = "&codec=h264";
				getById("codech264toggle").checked = true;
			} else if (codecGroupFlag.value === "vp8") {
				codecGroupFlag = "&codec=vp8";
				getById("codech264toggle").disabled = true;
			} else if (codecGroupFlag.value === "av1") {
				codecGroupFlag = "&codec=av1";
				getById("codech264toggle").disabled = true;
			} else {
				codecGroupFlag = "";
			}
		} else {
			codecGroupFlag = "";
		}

		session.codecGroupFlag = session.codecGroupFlag || codecGroupFlag || session.codecGroupFlag;
	}
	if (session.bitrateGroupFlag) {
		codecGroupFlag += session.bitrateGroupFlag;
	}

	stashRoomSession(broadcastFlag);

	formSubmitting = false;
	try {
		var m = getById("mainmenu");
		m.remove();
		document.querySelectorAll(".hidden2").forEach(ele2 => {
			ele2.classList.remove("hidden2");
		});
	} catch (e) {}

	getById("head1").className = "hidden";
	getById("head2").className = "hidden";
	getById("head4").className = "";

	try {
		if (session.label === false) {
			document.title = "Control Room";
		}
	} catch (e) {
		errorlog(e);
	}

	session.director = true;
	screensharesupport = false;

	if (session.meterStyle === false) {
		session.meterStyle = 1; // director specific style
	}
	if (session.signalMeter === null) {
		session.signalMeter = true;
	}
	if (session.batteryMeter === null) {
		session.batteryMeter = true;
	}

	if (session.directorPassword) {
		getById("coDirectorEnable").checked = true;
		getById("coDirectorEnableSpan").style.display = "none";

		var token = "";
		if (session.token) {
			token += "&token=" + session.token;
		}

		getById("codirectorSettings_invite").value = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token;
		if (session.password !== session.sitePassword) {
			if (session.password == false) {
				getById("codirectorSettings_invite").value += "&password=false";
			} else {
				getById("codirectorSettings_invite").value += "&password=" + session.password;
			}
		}

		if (session.codirector_transfer) {
			getById("codirectorSettings_transfer").checked = true;
		} else {
			getById("codirectorSettings_transfer").checked = false;
		}
		if (session.codirector_changeURL) {
			getById("codirectorSettings_changeurl").checked = true;
		} else {
			getById("codirectorSettings_changeurl").checked = false;
		}
		getById("codirectorSettings").style.display = "block";
	}

	window.onresize = updateMixer;
	window.onorientationchange = function () {
		if (Firefox) {
			updateForceRotate(true);
		}
		setTimeout(async function () {
			if (session.forceAspectRatio) {
				await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
			}
			if (session.effect && session.effect === "7") {
				digitalZoom();
			}
			updateForceRotate();
			updateMixer();
		}, 200);
	};
	getById("reshare").parentNode.removeChild(getById("reshare"));

	//getById("mutespeakerbutton").style.display = null;
	if (session.speakerMuted_default === false) {
		//session.speakerMuted = false; // the director will start with audio playback muted.
		toggleSpeakerMute(true); // let it be what it is.
	} else {
		session.speakerMuted = true; // the director will start with audio playback muted.
		toggleSpeakerMute(true); // okay since only run on start
	}

	var token = "";
	if (session.token) {
		token += "&token=" + session.token;
	}

	getById("director_block_1").dataset.raw = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token;
	getById("director_block_1").href = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token;
	getById("director_block_1").innerText = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token;

	getById("director_block_3").dataset.raw = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token;
	getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token;
	getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token;

	if (session.cleanDirector == false && session.cleanOutput == false) {
		getById("roomHeader").style.display = "";
		//getById("directorLinks").style.display = "";
		getById("directorLinks1").style.display = "inline-block";
		getById("directorLinks2").style.display = "inline-block";

		getById("calendarButton").style.display = "inline-block";
	} else {
		getById("guestFeeds").innerHTML = "";
	}

	getById("guestFeeds").style.display = "";

	if (!session.cleanOutput) {
		if (session.queue) {
			getById("queuebutton").classList.remove("hidden");
		}
		getById("chatbutton").classList.remove("hidden");
		getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though.
		getById("controlButtons").classList.remove("hidden");
		getById("mutespeakerbutton").classList.remove("hidden");
		getById("websitesharebutton").classList.remove("hidden");
		//getById("screensharebutton").classList.remove("hidden");

		if (session.totalRoomBitrate) {
			getById("roomsettingsbutton").classList.remove("hidden");
		}

		if (!session.showDirector) {
			// if null or false, we want to show the solo link, since the director won't have their control box. The director will be visible in their solo link
			getById("miniPerformer").innerHTML = '<button id="press2talk" onmousedown="event.preventDefault(); event.stopPropagation();" class="float" onclick="press2talk(true);" title="You can also enable the director`s Video Output afterwards by clicking the Setting`s button"><i class="las la-headset"></i><span data-translate="push-to-talk-enable"> enable director`s microphone or video<br />(only guests can see this feed)</span></button>';
			//miniTranslate(getById("miniPerformer"));
			getById("grabDirectorSoloLink").dataset.raw = "https://" + location.host + location.pathname + "?solo&r=" + session.roomid + "&v=" + session.streamID + passAdd2 + wss + token;
			getById("grabDirectorSoloLink").href = "https://" + location.host + location.pathname + "?solo&r=" + session.roomid + "&v=" + session.streamID + passAdd2 + wss + token;
			getById("grabDirectorSoloLink").innerText = "https://" + location.host + location.pathname + "?solo&r=" + session.roomid + "&v=" + session.streamID + passAdd2 + wss + token;
			getById("grabDirectorSoloLinkParent").classList.remove("hidden");
		} else {
			getById("miniPerformer").innerHTML = '<button id="press2talk" onmousedown="event.preventDefault(); event.stopPropagation();" class="float" onclick="press2talk(true);" title="You can also enable the director`s Video Output afterwards by clicking the Setting`s button"><i class="las la-headset"></i><span data-translate="push-to-talk-enable-2"> enable director`s microphone or video</span></button>';
		}
		miniTranslate(getById("miniPerformer"));

		getById("miniPerformer").className = "";

		var tabindex = 26;
		if (session.rooms && session.rooms.length > 0) {
			var container = getById("rooms");
			container.innerHTML += '<span style="display:inline-block">Arm Transfer: </span>';
			session.rooms.forEach(function (r) {
				// if(session.roomid == r) return; //don't include self
				container.innerHTML += '<button id="roomselect_' + r + '" onmousedown="event.preventDefault(); event.stopPropagation();"  style="display:inline-block" class="float btnArmTransferRoom" onclick="handleRoomSelect(\'' + r + '\');" title="Arm/disarm transfer to this room" tabindex="' + tabindex + '"><i class="las la-paper-plane"></i>' + r + "</button>";
				tabindex++;
			});
		}
	} else {
		getById("miniPerformer").style.display = "none";
		getById("controlButtons").classList.add("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.effect === false) {
		session.effect = null; // so the director can see the effects
	}

	getById("avatarDiv3").classList.remove("hidden"); // lets the director see the avatar option

	clearInterval(session.updateLocalStatsInterval);
	session.updateLocalStatsInterval = setInterval(function () {
		updateLocalStats();
	}, session.statsInterval);

	var directorWebsiteShare = getStorage("directorWebsiteShare"); // {"website":session.iframeSrc, "roomid":session.roomid}

	if (typeof directorWebsiteShare === "object" && directorWebsiteShare !== null && "website" in directorWebsiteShare) {
		if (directorWebsiteShare.website == false) {
			clearDirectorSettings();
		} else if (directorWebsiteShare.roomid && directorWebsiteShare.roomid == session.roomid) {
			session.iframeSrc = directorWebsiteShare.website;
			session.defaultIframeSrc = directorWebsiteShare.website;

			getById("websitesharebutton").classList.add("hidden");
			getById("websitesharebutton2").classList.remove("hidden");
		}
	}

	session.group.forEach(group => {
		// changeGroupDirectorAPI(group, state=null, update=true)
		changeGroupDirectorAPI(group, true, false); // update the UI only
	});

	session.groupView.forEach(group => {
		// changeGroupDirectorAPI(group, state=null, update=true)
		changeGroupViewDirectorAPI(group, true); // update the UI only
	});

	if (session.showDirector) {
		getById("highlightDirectorSpan").style.display = "none";
		getById("highlightDirectorSpan").remove();
	} else {
		getById("highlightDirector").dataset.sid = session.streamID;
	}

	setTimeout(() => {
		loadDirectorSettings();
		if (broadcastFlag) {
			saveDirectorSettings();
		}
	}, 100);

	joinRoom(session.roomid);

	pokeIframeAPI("create-room", session.roomid);

	try {
		if (!gotDevices2AlreadyRan && (iOS || iPad)) {
			await enumerateDevices().then(gotDevices2); // this is needed for iOS; was previous set to timeout at 100ms, but would be useful everywhere I think. (Breaks director's auto start, so just iOS for now)
		}
	} catch (e) {
		errorlog(e);
	}

	if (session.autostart) {
		setTimeout(function () {
			press2talk(true);
		}, 400);
	} else {
		session.seeding = true;
		session.seedStream();
	}
} // createRoomCallback

function handleRoomSelect(room) {
	var elems = document.querySelectorAll(".btnArmTransferRoom");
	[].forEach.call(elems, function (el) {
		el.classList.remove("selected");
	});
	if (previousRoom == room) {
		previousRoom = "";
		armedTransfer = false;
		stillNeedRoom = true;
	} else {
		previousRoom = room;
		stillNeedRoom = false;
		armedTransfer = true;
		getById("roomselect_" + room).classList.add("selected");
	}
}

function getDirectorSettings(scene = false) {
	var settings = {};

	var eles = document.querySelectorAll('[data-action-type="solo-video"]');
	settings.soloVideo = false;
	for (var i = 0; i < eles.length; i++) {
		if (eles[i].value == 1) {
			warnlog(eles[i]);
			if (eles[i].dataset.sid) {
				settings.soloVideo = eles[i].dataset.sid; // who is solo, if someone is solo
			}
		}
	}
	if (scene) {
		var eles = document.querySelectorAll('[data-action-type="addToScene"][data-scene="' + scene + '"');
		settings.scene = {};
		for (var i = 0; i < eles.length; i++) {
			if (eles[i].value == 1) {
				if (eles[i].dataset.sid) {
					var msg = {};
					msg.scene = scene;
					msg.action = "display";
					msg.value = eles[i].value;
					msg.target = eles[i].dataset.sid;

					settings.scene[eles[i].dataset.sid] = msg;
				}
			}
		}
	}

	settings.showDirector = session.showDirector;

	settings.mute = {};
	var eles = document.querySelectorAll('[data-action-type="mute-scene"]');
	for (var i = 0; i < eles.length; i++) {
		if (eles[i].value == 1) {
			// if muted
			if (eles[i].dataset.sid) {
				var msg = {};
				msg.action = "mute";
				msg.scene = true;
				msg.value = 1;
				msg.target = eles[i].dataset.sid;
				settings.mute[eles[i].dataset.sid] = msg;
			}
		}
	}
	return settings;
}

function requestInfocus(ele, evt = null, value = null) {
	try {
		var sid = ele.dataset.sid;
	} catch (e) {
		warnlog("no stream ID found; requestinfocus");
		var sid = false;
		if (ele.id === "highlightDirector") {
			if (session.streamID) {
				sid = session.streamID;
			}
		}
	}

	if (value !== null) {
		if (value) {
			ele.value == 0; // we will toggle it in a second anyways.
		} else {
			ele.value == 1;
		}
	}

	var special = false;
	if (evt) {
		special = evt.ctrlKey || evt.metaKey || false;
		if (special) {
			special = true;
		}
	}

	if (ele.value == 1) {
		ele.value = 0;
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		ele.classList.remove("altpress");
		var actionMsg = {};
		actionMsg.infocus = false;
		//session.sendMessage(actionMsg);
	} else {
		var actionMsg = {};
		if (special) {
			actionMsg.infocus2 = sid;
		} else {
			actionMsg.infocus = sid;
		}
		//session.sendMessage(actionMsg);

		var eles = document.querySelectorAll('[data-action-type="solo-video"]');
		for (var i = 0; i < eles.length; i++) {
			log(eles);
			eles[i].classList.remove("pressed");
			eles[i].ariaPressed = "false";
			eles[i].classList.remove("altpress");
			eles[i].value = 0;
		}
		ele.value = 1;

		if (special) {
			ele.classList.add("altpress");
		} else {
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
		}
		if (ele.id !== "highlightDirector") {
			getById("highlightDirector").checked = false;
		}
	}

	for (var uuid in session.pcs) {
		if (!session.pcs[uuid].solo && !session.pcs[uuid].layout) {
			// only issue highlight commands to non-solo and non-layout links
			session.sendMessage(actionMsg, uuid);
		}
	}

	syncDirectorState(ele);

	if (ele.value == 1) {
		return true;
	} else {
		return false;
	}
}

var fixScrollReset = null;
var fixScrollResetValue = null;

function requestAudioSettings(ele) {
	var UUID = ele.dataset.UUID;

	try {
		clearTimeout(fixScrollReset);
		fixScrollResetValue = getById("directorlayout").scrollTop;
		fixScrollReset = setTimeout(
			function (scrollpos) {
				fixScrollReset = null;
				getById("directorlayout").scrollTop = scrollpos;
			},
			1000,
			fixScrollResetValue
		);

		query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").value = 0;
		query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").classList.remove("pressed");
		query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").ariaPressed = "false";
		query("#container_" + UUID + " .advancedVideoSettings").classList.add("hidden");
		query("#container_" + UUID + " .advancedVideoSettings").innerHTML = "";
	} catch (e) {}

	if (ele.value == 1) {
		ele.value = 0;
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		query("#container_" + UUID + " .advancedAudioSettings").classList.add("hidden");
		query("#container_" + UUID + " .advancedAudioSettings").innerHTML = "";
		return false;
	} else {
		ele.value = 1;
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
		query("#container_" + UUID + " .advancedAudioSettings").innerHTML = "";
		var actionMsg = {};
		actionMsg.getAudioSettings = true;
		session.sendRequest(actionMsg, UUID);
		return true;
	}
}

function requestVideoSettings(ele) {
	var UUID = ele.dataset.UUID;

	try {
		clearTimeout(fixScrollReset);
		fixScrollResetValue = getById("directorlayout").scrollTop;
		fixScrollReset = setTimeout(
			function (scrollpos) {
				fixScrollReset = null;
				getById("directorlayout").scrollTop = scrollpos;
			},
			1000,
			fixScrollResetValue
		);

		query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").value = 0;
		query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").classList.remove("pressed");
		query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").ariaPressed = "false";
		query("#container_" + UUID + " .advancedAudioSettings").classList.add("hidden");
		query("#container_" + UUID + " .advancedAudioSettings").innerHTML = "";
	} catch (e) {}

	if (ele.value == 1) {
		ele.value = 0;
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		query("#container_" + UUID + " .advancedVideoSettings").classList.add("hidden");
		query("#container_" + UUID + " .advancedVideoSettings").innerHTML = "";
		return false;
	} else {
		ele.value = 1;
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
		query("#container_" + UUID + " .advancedVideoSettings").innerHTML = "";
		var actionMsg = {};
		actionMsg.getVideoSettings = true;
		session.sendRequest(actionMsg, UUID);
		return true;
	}
}

function combinedLayoutSimple(layout) {
    var combined = {};
    Object.keys(layout).forEach(i => {
        if (!layout[i]) {
            return;
        }
        
        if (i === "") {
            layout[i].forEach(j => {
                if (!j) {
                    return;
                }
                var streamID = null;
                if ("slot" in j) {
                    try {
                        streamID = session.currentSlots[parseInt(j.slot) + 1];
                    } catch (e) {
                        errorlog(e);
                        streamID = null;
                    }
                }
                if (!streamID) {
                    if (!combined[""]) {
                        combined[""] = [];
                    }
                    combined[""].push(j);
                } else {
                    combined[streamID] = j;
                }
            });
        } else {
            var streamID = null;
            if ("slot" in layout[i]) {
                try {
                    streamID = session.currentSlots[parseInt(layout[i].slot) + 1];
                } catch (e) {
                    errorlog(e);
                    streamID = null;
                }
            }
            if (!streamID) {
                if (!combined[""]) {
                    combined[""] = [];
                }
                combined[""].push(layout[i]);
            } else {
                combined[streamID] = layout[i];
            }
        }
    });
    return combined;
}



async function createDirectorOnlyBox() {
	var soloLink = soloLinkGenerator(session.streamID);

	if (document.getElementById("deleteme")) {
		getById("deleteme").parentNode.removeChild(getById("deleteme"));
	}
	var controls = getById("controls_directors_blank").cloneNode(true);
	controls.classList.remove("hidden");
	controls.id = "controls_director";

	var container = document.createElement("div");
	container.className = "vidcon directorMargins";
	container.id = "container_director"; // needed to delete on user disconnect
	container.setAttribute("aria-label", miscTranslations["your-camera"]);
	container.setAttribute("role", "region");
	
	var buttons = "";
	if (session.slotmode) {
		var biggestSlot = 0;
		var slotDefault = null;
		
		// Check past slots first
		if (session.streamID in session.pastSlots) {
			slotDefault = session.pastSlots[session.streamID];
		}
		
		// Get all current slots from RPC state
		var allSlots = [];
	    if (session.slotmode == 1) {
			Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => {
				if (currentSlot) {
					if (parseInt(currentSlot) > biggestSlot) {
						biggestSlot = parseInt(currentSlot);
					}
					if (slotDefault === parseInt(currentSlot)) {
						slotDefault = null;
					}
					allSlots.push(parseInt(currentSlot));
				}
			});
			biggestSlot += 1;
		} else if (slotDefault !== null && session.slotmode == 2) {
			// Check if slot is already in use - include director's stream
			const slotInUse = Object.keys(session.rpcs).some(UUID => 
				getSlotState(UUID) === slotDefault
			) || getSlotState(session.streamID) === slotDefault || getSlotState(session.streamID+":s") === slotDefault;
			
			if (slotInUse) {
				slotDefault = null; // This slot is already in use
			}
		}
		
		// Determine final slot value
		if (slotDefault !== null) {
			biggestSlot = slotDefault;
		} else if (session.slotmode == 1) {
			var bestfree = 0;
			for (var i = 1; i <= biggestSlot; i++) {
				if (allSlots.includes(i)) {
					continue;
				} else {
					bestfree = i;
					break;
				}
			}
			biggestSlot = bestfree;
		}
		
		// Set slot name
		var slotName = biggestSlot ? "slot: " + biggestSlot : "unset";
		
		// Build HTML with same structure
		buttons +=
			`<div draggable='true' title='Drag to swap layout positions' 
					ondragend='dragendSlot(event)' 
					ondragstart='dragSlot(event)' 
					ondrop='dropSlot(event)' 
					ondragover='allowDropSlot(event)' 
					data-sid='${session.streamID}' 
					data-slot='${biggestSlot}' 
					class='slotsbar'>
				<button ondrop='dropSlot(event)' 
						data-action-type='setslot' 
						draggable='true' 
						ondragend='dragendSlot(event)' 
						ondragstart='dragSlot(event)' 
						ondragover='allowDropSlot(event)' 
						onclick='changeSlot(event, this);'>${slotName}</button>
			 </div>`;

		// Sync the initial state
		syncSlotState(session.streamID, biggestSlot, false); // false since UI is being created here
	}
	buttons +=
		"<div title='Does not impact scene order.' class='shift'><i class='las la-angle-left' onclick='shiftPC(this,-1, true);'></i><i class='las la-angle-right' onclick='shiftPC(this,1, true)';></i></div>\
		<div class='streamID' style='user-select: none;'>ID: <span style='user-select: text;'>" +
		session.streamID +
		"</span>\
			<i class='las la-copy' data-sid='" +
		session.streamID +
		"'  onclick='copyFunction(this.dataset.sid,event)' title='Copy this Stream ID to the clipboard' style='cursor:pointer'></i>\
			<i class='las la-window-minimize' onclick='minimizeMe(this, 'container_director')' title='Minimize this control box' style='cursor:pointer'></i>\
			<span id='label_director' class='addALabel' title='Click here to edit the label for this stream. Changes will propagate to all viewers of this stream' data-translate='add-a-label'>" +
		getTranslation("add-a-label") +
		"</span>\
		</div>\
		<div id='videoContainer_director'></div>";

	container.innerHTML = buttons;

	var oldGroups = [];
	document.querySelectorAll("#groups [data-action-type='toggle-group'][data-group]:not(.green)").forEach(ee => {
		oldGroups.push(ee.dataset.group);
	});
	getById("groups").remove();

	if (session.hidesololinks == false) {
		// won't be updating the solo link to a view-only one ever, since director is always expected to be in a room
		controls.innerHTML +=
			"<div class='soloButton' title='A direct solo view of the video/audio stream with nothing else'> \
				<a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \
				value='" +
			soloLink +
			"' href='" +
			soloLink +
			"'/>" +
			sanitizeChat(soloLink) +
			"</a>\
				<button class='pull-right controlsGrid' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span data-translate='copy-solo-view-link'>copy solo view link</span></button>\
			</div>\
			<div id='groups'></div>";
		if (session.directorUUID) {
			controls.innerHTML += "<div style='text-align: center;margin:10px 5px 5px 5px;display:block;'><h3 id='yourDirectorStatus'>This is you, a co-director.<br />You are also a performer.</h3></div>";
		} else {
			controls.innerHTML += "<div style='text-align: center;margin:10px 5px 5px 5px;display:block;'><h3 id='yourDirectorStatus'>This is you, the director.<br />You are also a performer.</h3></div>";
		}
	}

	controls.querySelectorAll("[data-action-type]").forEach(ele => {
		// give action buttons some self-reference
		ele.dataset.sid = session.streamID;
	});

	container.appendChild(controls);

	getById("guestFeeds").appendChild(container);

	Object.keys(session.sceneList).forEach((scene, index) => {
		if (document.getElementById("container_director")) {
			if (!getById("container_director").querySelectorAll('[data-scene="' + scene + '"]').length) {
				var newScene = document.createElement("div");
				newScene.innerHTML = '<button style="margin: 0 5px 10px 5px;" data-sid="' + session.streamID + '" data-action-type="addToScene" data-scene="' + scene + '"   title="Add to Scene ' + scene + '" onclick="directEnable(this, event);"><span ><i class="las la-plus-square" style="color:#060"></i> Scene: ' + scene + "</span></button>";
				newScene.classList.add("customScene");
				//getById("container_director").appendChild(newScene);

				var added = false;
				getById("container_director")
					.querySelectorAll(".customScene>[data-scene]")
					.forEach(ele => {
						if (!added && ele.dataset.scene > scene + "") {
							ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode);
							added = true;
						}
					});
				if (!added) {
					getById("container_director").appendChild(newScene);
				}
			}
		}
	});

	getById("groups").showDirector = true;

	session.group.forEach(group => {
		// changeGroupDirectorAPI(group, state=null, update=true)
		changeGroupDirectorAPI(group, true, false); // update the UI only /
	});

	oldGroups.forEach(group => {
		// changeGroupDirectorAPI(group, state=null, update=true)
		changeGroupDirectorAPI(group, false, false); // update the UI only /
	});

	var labelID = document.getElementById("label_director");

	labelID.onclick = async function (ee) {
		var oldlabel = ee.target.innerText;
		if (session.label === false) {
			oldlabel = "";
		}
		window.focus();
		var newlabel = await promptAlt(getTranslation("enter-new-display-name"), false, false, oldlabel);
		if (newlabel !== null) {
			newlabel = newlabel.trim();
			if (newlabel === "") {
				newlabel = false;
				//ee.target.innerHTML = getTranslation("add-a-label");
				miniTranslate(ee.target, "add-a-label");
				ee.target.classList.add("addALabel");
			} else {
				ee.target.innerText = newlabel;
				ee.target.classList.remove("addALabel");
			}
			session.label = newlabel;
			var data = {};
			data.changeLabel = true;
			data.value = session.label;
			session.sendMessage(data);
			stashRoomSession();
		}
	};
	labelID.style.float = "left";
	labelID.style.top = "2px";
	labelID.style.marginLeft = "5px";
	labelID.style.position = "relative";
	labelID.style.cursor = "pointer";
	if (session.label) {
		labelID.innerText = session.label;
	}
	pokeIframeAPI("control-box", true, true);
	
}


async function createDirectorScreenshareOnlyBox() {
	// sstype=3
	var screenStreamID = session.streamID + ":s";
	var soloLink = soloLinkGenerator(screenStreamID);

	if (document.getElementById("deleteme")) {
		getById("deleteme").parentNode.removeChild(getById("deleteme"));
	}
	var controls = getById("controls_directors_blank").cloneNode(true);
	controls.classList.remove("hidden");
	controls.id = "controls_screen_director";

	var container = document.createElement("div");
	container.className = "vidcon directorMargins";
	container.id = "container_screen_director"; // needed to delete on user disconnect
	container.setAttribute("aria-label", miscTranslations["your-screenshare"]);
	container.setAttribute("role", "region");

	var buttons = "";
	if (session.slotmode) {
		var biggestSlot = 0;
		var slotDefault = null;
		
		if (screenStreamID in session.pastSlots) {
			slotDefault = session.pastSlots[screenStreamID];
		}
		
		var allSlots = [];
	    if (session.slotmode == 1) {
		    Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => {
		  	    if (currentSlot) {
				   if (parseInt(currentSlot) > biggestSlot) {
					   biggestSlot = parseInt(currentSlot);
				   }
				   if (slotDefault === parseInt(currentSlot)) {
					   slotDefault = null;
				   }
				   allSlots.push(parseInt(currentSlot));
			    }
		    });
		    biggestSlot += 1;
	    }
		
		// Determine final slot value
		if (slotDefault !== null) {
			biggestSlot = slotDefault;
		} else if (session.slotmode == 1) {
			var bestfree = 0;
			for (var i = 1; i <= biggestSlot; i++) {
				if (allSlots.includes(i)) {
					continue;
				} else {
					bestfree = i;
					break;
				}
			}
			biggestSlot = bestfree;
		}
		
		var slotName = biggestSlot ? "slot: " + biggestSlot : "unset";
		
		buttons +=
			`<div draggable='true' title='Drag to swap layout positions' 
				 ondragend='dragendSlot(event)' 
				 ondragstart='dragSlot(event)' 
				 ondrop='dropSlot(event)' 
				 ondragover='allowDropSlot(event)' 
				 data-sid='${screenStreamID}' 
				 data-slot='${biggestSlot}' 
				 class='slotsbar'>
			   <button ondrop='dropSlot(event)' 
					   data-action-type='setslot' 
					   draggable='true' 
					   ondragend='dragendSlot(event)' 
					   ondragstart='dragSlot(event)' 
					   ondragover='allowDropSlot(event)' 
					   onclick='changeSlot(event, this);'>${slotName}</button>
			 </div>`;

		// Sync the initial state
		syncSlotState(screenStreamID, biggestSlot, false); // false since UI is being created here
	}
	buttons +=
		"<div title='Does not impact scene order.' class='shift'><i class='las la-angle-left' onclick='shiftPC(this,-1, true);'></i><i class='las la-angle-right' onclick='shiftPC(this,1, true)';></i></div>\
		<div class='streamID' style='user-select: none;'>ID: <span style='user-select: text;'>" +
		screenStreamID +
		"</span>\
			<i class='las la-copy' data-sid='" +
		screenStreamID +
		"'  onclick='copyFunction(this.dataset.sid,event)' title='Copy this Stream ID to the clipboard' style='cursor:pointer'></i>\
			<i class='las la-window-minimize' onclick='minimizeMe(this, 'container_screen_director')' title='Minimize this control box'></i>\
			<span id='label_director' class='addALabel' title='Click here to edit the label for this stream. Changes will propagate to all viewers of this stream' data-translate='add-a-label'>" +
		getTranslation("add-a-label") +
		"</span>\
		</div>\
		<div id='videoScreenContainer_director'></div>";

	container.innerHTML = buttons;

	var oldGroups = [];
	document.querySelectorAll("#groups [data-action-type='toggle-group'][data-group]:not(.green)").forEach(ee => {
		oldGroups.push(ee.dataset.group);
	});
	getById("groups").remove();

	if (session.hidesololinks == false) {
		// won't be updating the solo link to a view-only one ever, since director is always expected to be in a room
		controls.innerHTML +=
			"<div class='soloButton' title='A direct solo view of the video/audio stream with nothing else'> \
				<a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \
				value='" +
			soloLink +
			"' href='" +
			soloLink +
			"'/>" +
			sanitizeChat(soloLink) +
			"</a>\
				<button class='pull-right controlsGrid' style='width:100%' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span data-translate='copy-solo-view-link'>copy solo view link</span></button>\
			</div>\
			<div id='groups'></div>";
		if (session.directorUUID) {
			controls.innerHTML += "<div style='text-align: center;margin:10px 5px 5px 5px;display:block;'><h3 id='yourDirectorStatusSS'>This is you, a co-director.<br />You are also a performer.</h3></div>";
		} else if (session.showDirector === false) {
			try {
				controls.querySelectorAll('[data-action-type="addToScene"]').forEach(ele => {
					ele.classList.add("hidden");
				});
			} catch (e) {
				errorlog(e);
			}
			controls.innerHTML += "<div style='text-align: center;margin:10px 5px 5px 5px;display:block;'><h3 id='yourDirectorStatusSS'>This is your screen share<br />It's *not* a performer.</h3></div>";
		} else {
			controls.innerHTML += "<div style='text-align: center;margin:10px 5px 5px 5px;display:block;'><h3 id='yourDirectorStatusSS'>This your screen share.<br />It's also a performer.</h3></div>";
		}
	}

	controls.querySelectorAll("[data-action-type]").forEach(ele => {
		// give action buttons some self-reference
		ele.dataset.sid = screenStreamID;
	});

	container.appendChild(controls);

	getById("guestFeeds").appendChild(container);

	Object.keys(session.sceneList).forEach((scene, index) => {
		if (document.getElementById("container_screen_director")) {
			if (!getById("container_screen_director").querySelectorAll('[data-scene="' + scene + '"]').length) {
				var newScene = document.createElement("div");
				newScene.innerHTML = '<button style="margin: 0 5px 10px 5px;" data-sid="' + screenStreamID + '" data-action-type="addToScene" data-scene="' + scene + '"   title="Add to Scene ' + scene + '" onclick="directEnable(this, event);"><span ><i class="las la-plus-square" style="color:#060"></i> Scene: ' + scene + "</span></button>";
				newScene.classList.add("customScene");
				//getById("container_screen_director").appendChild(newScene);

				var added = false;
				getById("container_screen_director")
					.querySelectorAll(".customScene>[data-scene]")
					.forEach(ele => {
						if (!added && ele.dataset.scene > scene + "") {
							ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode);
							added = true;
						}
					});
				if (!added) {
					getById("container_screen_director").appendChild(newScene);
				}
			}
		}
	});

	getById("groups").showDirector = true;

	session.group.forEach(group => {
		// changeGroupDirectorAPI(group, state=null, update=true)
		changeGroupDirectorAPI(group, true, false); // update the UI only /
	});

	oldGroups.forEach(group => {
		// changeGroupDirectorAPI(group, state=null, update=true)
		changeGroupDirectorAPI(group, false, false); // update the UI only /
	});

	document.querySelectorAll("#container_screen_director  #label_director").forEach(elex => {
		elex.remove();
	});

	pokeIframeAPI("control-box", true, true);
	
}

function shiftPC(ele, shift, director = false) {
	if (director) {
		var target = document.getElementById("container_director");
	} else {
		var target = document.getElementById("container_" + ele.dataset.UUID);
	}

	if (!target) {
		return;
	}

	target.shifted = true;

	var target2 = false;

	if (shift == 1) {
		if (target.nextSibling) {
			target2 = target.nextSibling;
			target.parentNode.insertBefore(target.nextSibling, target);
		}
	} else {
		if (target.previousSibling) {
			target2 = target.previousSibling;
			target.parentNode.insertBefore(target, target.previousSibling);
		}
	}
	updateLockedElements();

	if (session.api) {
		var slots = {};
		var elements = getById("guestFeeds").children;
		for (var i = 0; i < elements.length; i++) {
			if (elements[i] === target) {
				var tmp = target.querySelector("[data-sid]");
				if (tmp) {
					var lock = target.querySelector("[data-locked]");
					if (lock) {
						lock = parseInt(lock.dataset.locked);
					}
					tmp = tmp.dataset.sid;
					slots[tmp] = lock || i + 1;
				}
			} else if (elements[i] === target2) {
				if (tmp) {
					var lock = target2.querySelector("[data-locked]");
					if (lock) {
						lock = parseInt(lock.dataset.locked);
					}
					tmp = tmp.dataset.sid;
					slots[tmp] = lock || i + 1;
				}
			}
		}
		pokeAPI("positionChange", slots);
	}
}

function updateLockedElements() {
	var eles = getById("guestFeeds").children;
	for (var i = 0; i < eles.length; i++) {
		try {
			var UUID = eles[i].UUID;
			var lock = document.getElementById("position_" + UUID).dataset.locked;
			if (parseInt(lock)) {
				lockPosition(document.getElementById("position_" + UUID), true);
			}
		} catch (e) {}
	}
}

function lockPosition(ele, apply = false) {
	var UUID = ele.dataset.UUID;
	if (apply) {
		if (ele.dataset.locked && parseInt(ele.dataset.locked)) {
			if (getById("guestFeeds")) {
				var currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1;
				ele.innerHTML = "<b>#" + ele.dataset.locked + "</b>";
				ele.parentNode.classList.add("locked");

				while (currentPosition > parseInt(ele.dataset.locked)) {
					var node = document.getElementById("container_" + UUID);
					(parent = node.parentNode), (prev = node.previousSibling), (oldChild = parent.removeChild(node));
					parent.insertBefore(oldChild, prev);
					currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1;
				}

				while (currentPosition < parseInt(ele.dataset.locked) && getById("guestFeeds").children.length > currentPosition) {
					var node = document.getElementById("container_" + UUID);
					(parent = node.parentNode), (next = node.nextSibling), (oldChild = parent.removeChild(node));
					parent.insertBefore(node, next.nextSibling);
					currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1;
				}
			}
		} else {
			ele.dataset.locked = 0;
			ele.innerHTML = "<i class='las la-lock-open'></i>";
			ele.parentNode.classList.remove("locked");
		}
	} else {
		if (ele.dataset.locked && parseInt(ele.dataset.locked)) {
			ele.dataset.locked = 0;
			ele.innerHTML = "<i class='las la-lock-open'></i>";
			ele.parentNode.classList.remove("locked");
		} else {
			if (getById("guestFeeds")) {
				ele.dataset.locked = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1;
				ele.innerHTML = "<b>#" + ele.dataset.locked + "</b>";
				ele.parentNode.classList.add("locked");
			}
		}
	}
}

function allowDropSlot(event) {
	event.preventDefault();
}

function dragSlot(event) {
	log("drag");

	var ele = event.target;
	if (!ele.dataset.sid && ele.parentNode.dataset.sid) {
		ele = ele.parentNode;
	}

	event.dataTransfer.setDragImage(getById("dragImage"), 24, 24);
	event.dataTransfer.setData("text", ele.dataset.sid);

	var eles = document.querySelectorAll(".slotsbar");
	for (var i = 0; i < eles.length; i++) {
		if (eles[i].dataset.sid == ele.dataset.sid) {
			continue;
		}
		eles[i].style.boxShadow = "0px 0px 8px 2px #FFF";
	}
}

function dragendSlot(event) {
	var eles = document.querySelectorAll(".slotsbar");
	for (var i = 0; i < eles.length; i++) {
		eles[i].style.boxShadow = "unset";
	}
	return true;
}

function dropSlot(event) {
    log("drop");
    event.preventDefault();
    event.stopPropagation();

    // Get the dragged streamID
    var SID = event.dataTransfer.getData("text");
    if (!SID) return;
    
    var origThing = document.querySelector("[data-sid='" + SID + "'][data-slot]");
    if (!origThing) return;

    // Get target streamID
    var targetSID = event.target.dataset.sid || event.target.parentNode.dataset.sid;
    if (!targetSID) return;
    
    var targetThing = document.querySelector("[data-sid='" + targetSID + "'][data-slot]");
    if (!targetThing) return;

    // Get original slots
    const origSlot = parseInt(origThing.dataset.slot);
    const targetSlot = parseInt(targetThing.dataset.slot);
    
    // Key fix: We need to swap the DOM elements *and* swap the session.currentSlots entries
    // Save the original values
    const tempStreamID = session.currentSlots[targetSlot]; // Save the target slot's original value
    
    // Update session.currentSlots (this is the crucial part)
    session.currentSlots[targetSlot] = SID;
    session.currentSlots[origSlot] = tempStreamID;
    
    // Update the data-sid attributes for the visual swap
    targetThing.dataset.sid = SID;
    origThing.dataset.sid = targetSID;
    
    // Update the UI text as well
    const targetButton = targetThing.querySelector('button');
    const origButton = origThing.querySelector('button');
    
    if (targetButton) {
        targetButton.innerText = targetSlot ? `slot: ${targetSlot}` : 'unset';
    }
    
    if (origButton) {
        origButton.innerText = origSlot ? `slot: ${origSlot}` : 'unset';
    }
    
    // Update past slots for future reference
    session.pastSlots[SID] = targetSlot;  // we don't need to run syncSlotState(), as this handles it
    session.pastSlots[targetSID] = origSlot;
    
    // Tell any iframes about the swap
    pokeIframeAPI("slot-updated", targetSlot, null, SID);
    pokeIframeAPI("slot-updated", origSlot, null, targetSID);
    
    // Notify all peers of the update
	broadcastSlotUpdate();
    
    return false;
}

function dragenterSlot(event) {
	event.preventDefault();
	if (event.target.classList.contains("slotsbar")) {
		event.target.style.border = "3px dotted black";
	}
}

function dragleaveSlot(event) {
	event.preventDefault();
	if (event.target.classList.contains("slotsbar")) {
		event.target.style.border = "";
	}
}

async function changeSlot(event, ele) { 
	var picker = document.getElementById("slotPicker");

	if (picker) {
		clearTimeout(modalTimeout);

		if (document.getElementById("modalBackdrop")) {
			getById("alertModal").innerHTML = ""; // Delete modal
			getById("alertModal").remove();
			getById("modalBackdrop").innerHTML = ""; // Delete modal
			getById("modalBackdrop").remove();
		}

		zindex = 31 + document.querySelectorAll(".alertModal").length;
		message = picker.innerHTML;

		modalTemplate = `<div class="alertModal" id="alertModal" style="text-align: center;z-index:${zindex + 2}">	
				<div class="alertModalInner">
					<span class='modalClose' onclick="closeModal()">×</span>
					<span id="alertModalMessage" class='alertModalMessage'>${message}</span>
				</div>
			</div>
			<div id="modalBackdrop" style="z-index:${zindex + 1}"></div>`;
		document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end

		document.getElementById("modalBackdrop").addEventListener("click", closeModal);

		document
			.getElementById("alertModalMessage")
			.querySelectorAll("div[data-slot]")
			.forEach(choice => {
				choice.onclick = function () {
					setSlot(ele, parseInt(this.dataset.slot));
					closeModal();
				};
			});

		if (event) {
			document.getElementById("alertModal").style.left = event.pageX + "px"; // 165px 356px
			document.getElementById("alertModal").style.top = event.pageY + 180 + "px";
		}

		getById("alertModal").addEventListener("click", function (e) {
			e.stopPropagation();
			return false;
		});
	} else {
		var slot = await promptAlt("Which slot to change to?");
		setSlot(ele, slot);
	}
}

function setSlot(ele, slot) {
    log("setSlot()");
    getById("slotPicker").classList.add("hidden");
    if (slot !== null) {
        try {
            slot = parseInt(slot) || 0;
            
            // Find container with stream ID
            const container = ele.closest('[data-sid]');
            const streamID = container ? container.dataset.sid : null;
            
            if (!streamID) {
                return false;
            }
            
            // Critical part: Check if the slot is already occupied and swap instead of replace
            const existingStreamID = session.currentSlots[slot];
            if (existingStreamID && existingStreamID !== streamID) {
                // Find the current slot of the stream we're moving
                let currentSlot = null;
                Object.entries(session.currentSlots).forEach(([key, value]) => {
                    if (value === streamID) {
                        currentSlot = parseInt(key);
                    }
                });
                
                // If the stream we're moving is already in a slot, update that slot's value
                if (currentSlot !== null) {
                    // Perform the swap
                    session.currentSlots[slot] = streamID;
                    session.currentSlots[currentSlot] = existingStreamID;
                    
                    // Update the other element's UI
                    const otherSlotBar = document.querySelector(`[data-sid="${existingStreamID}"][data-slot]`);
                    if (otherSlotBar) {
                        otherSlotBar.dataset.slot = currentSlot;
                        const otherButton = otherSlotBar.querySelector('button');
                        if (otherButton) {
                            otherButton.innerText = currentSlot ? `slot: ${currentSlot}` : 'unset';
                        }
                    }
                    
                    // Update UI for the element we're setting
                    container.dataset.slot = slot;
                    ele.innerText = slot ? `slot: ${slot}` : 'unset';
                    
                    // Update pastSlots
                    session.pastSlots[streamID] = slot;
                    session.pastSlots[existingStreamID] = currentSlot;
                    
                    // Update iframes
                    pokeIframeAPI("slot-updated", slot, null, streamID);
                    pokeIframeAPI("slot-updated", currentSlot, null, existingStreamID);
                } else {
                    // We're moving a stream that wasn't in a slot before
                    // First clear any current assignment for this stream
                    Object.entries(session.currentSlots).forEach(([key, value]) => {
                        if (value === streamID) {
                            delete session.currentSlots[key];
                        }
                    });
                    
                    // Then assign it to the new slot
                    session.currentSlots[slot] = streamID;
                    
                    // Update UI
                    container.dataset.slot = slot;
                    ele.innerText = slot ? `slot: ${slot}` : 'unset';
                    
                    // Update pastSlots
                    session.pastSlots[streamID] = slot;
                    
                    // Update iframe
                    pokeIframeAPI("slot-updated", slot, null, streamID);
                }
            } else {
                // No conflict, just set the slot normally
                // Clear any existing slot for this stream
                Object.entries(session.currentSlots).forEach(([key, value]) => {
                    if (value === streamID) {
                        delete session.currentSlots[key];
                    }
                });
                
                // Set the new slot
                if (slot) {
                    session.currentSlots[slot] = streamID;
                }
                
                // Update UI
                container.dataset.slot = slot;
                ele.innerText = slot ? `slot: ${slot}` : 'unset';
                
                // Update pastSlots
                session.pastSlots[streamID] = slot;
                
                // Update iframe
                pokeIframeAPI("slot-updated", slot, null, streamID);
            }
            
            // Always update all peers
			broadcastSlotUpdate();
            
        } catch (e) {
            errorlog(e);
            return false;
        }
        return true;
    }
    return false;
}

function swapNodes(n1, n2) {
	log("swapping nodes");
	var p1 = n1.parentNode;
	var p2 = n2.parentNode;
	var i1, i2;

	if (!p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1)) return;

	for (var i = 0; i < p1.children.length; i++) {
		if (p1.children[i].isEqualNode(n1)) {
			i1 = i;
		}
	}
	for (var i = 0; i < p2.children.length; i++) {
		if (p2.children[i].isEqualNode(n2)) {
			i2 = i;
		}
	}

	if (p1.isEqualNode(p2) && i1 < i2) {
		i2++;
	}
	p1.insertBefore(n2, p1.children[i1]);
	p2.insertBefore(n1, p2.children[i2]);
}

function getCurrentSlot(streamID) {
    const slotEntry = Object.entries(session.currentSlots).find(([_, sid]) => sid === streamID);
    return slotEntry ? slotEntry[0] : false;
}

function getSlotState(UUID) {
    if (!UUID || !(UUID in session.rpcs)) return false;
    return getCurrentSlot(session.rpcs[UUID].streamID);
}

function combinedLayout(layout) {
    if (!Array.isArray(layout)) return layout || {};
    
    var combined = {};
    for (var i = 0; i < layout.length; i++) {
        if (!layout[i] || !("slot" in layout[i])) {
            if (!combined[""]) combined[""] = [];
            combined[""].push(layout[i]);
            continue;
        }
        
        const slotNumber = parseInt(layout[i].slot || 0) + 1;
        const streamID = session.currentSlots[slotNumber];
        
        if (!streamID) {
            if (!combined[""]) combined[""] = [];
            combined[""].push(layout[i]);
            continue;
        }
        
        combined[streamID] = layout[i];
    }
    return combined;
}

function syncSlotState(streamID, slotValue=false, updateUI=true) {
    // Clear any existing slots for this stream
    Object.entries(session.currentSlots).forEach(([slot, sid]) => {
        if (sid === streamID) delete session.currentSlots[slot];
    });
    
    // Set new slot if one provided
    if (slotValue) {
        session.currentSlots[slotValue] = streamID;
    }
    
    // Update UI if requested
    if (updateUI) {
        const slotsBar = document.querySelector(`[data-sid="${streamID}"][data-slot]`);
        if (slotsBar) {
            slotsBar.dataset.slot = slotValue;
            const slotButton = slotsBar.querySelector('button');
            if (slotButton) {
                slotButton.innerText = slotValue ? `slot: ${slotValue}` : 'unset';
            }
        }
    }
	
	pokeIframeAPI("slot-updated", slotValue, null, streamID); // need to support self-director
	session.pastSlots[streamID] = slotValue || 0;
    
	clearTimeout(session.slotBroadcastThrottle);
    session.slotBroadcastThrottle = setTimeout(function(){broadcastSlotUpdate();},10);
    return true;
}
function broadcastSlotUpdate(UUID = false) {
    try {
		if (!session.slotmode || !session.director){
			return;
		}
        if (!UUID) {
			if (session.slotBroadcastThrottle){
				clearTimeout(session.slotBroadcastThrottle);
				session.slotBroadcastThrottle = null;
			}
            session.sendMessage({ slotsUpdate: session.currentSlots });
        } else {
            session.sendMessage({ slotsUpdate: session.currentSlots }, UUID);
        }
    } catch (e) {
        errorlog(e);
    }
}
function updateSlotUI() {
    // Update all slot UI elements based on the current state in session.currentSlots
    Object.entries(session.currentSlots).forEach(([slot, streamID]) => {
        const slotBar = document.querySelector(`[data-sid="${streamID}"][data-slot]`);
        if (slotBar) {
            slotBar.dataset.slot = slot;
            const button = slotBar.querySelector('button');
            if (button) {
                button.innerText = slot ? `slot: ${slot}` : 'unset';
            }
        }
    });
}

function createControlBox(UUID, soloLink, streamID, slot_init = false) {
	if (document.getElementById("deleteme")) {
		getById("deleteme").parentNode.removeChild(getById("deleteme"));
	}
	var controls = getById("controls_blank").cloneNode(true);
	controls.classList.remove("hidden");
	controls.id = "controls_" + UUID;

	var container = document.createElement("div");
	container.className = "vidcon directorMargins";
	container.id = "container_" + UUID; // needed to delete on user disconnect
	container.UUID = UUID;
	container.dataset.UUID = UUID;
	container.dataset.sid = streamID;

	if (session.orderby) {
		try {
			var added = false;
			for (var i = 0; i < getById("guestFeeds").children.length; i++) {
				if (getById("guestFeeds").children[i].UUID && !getById("guestFeeds").children[i].shifted) {
					if (getById("guestFeeds").children[i].UUID in session.rpcs) {
						if (session.rpcs[getById("guestFeeds").children[i].UUID].streamID.toLowerCase() > streamID.toLowerCase()) {
							getById("guestFeeds").insertBefore(container, getById("guestFeeds").children[i]);
							added = true;
							break;
						}
					}
				}
			}
			if (!added) {
				getById("guestFeeds").appendChild(container);
			}
		} catch (e) {
			getById("guestFeeds").appendChild(container);
		}
	} else {
		getById("guestFeeds").appendChild(container);
	}
	
	//controls.innerHTML += "<div id='advanced_audio_director_" + UUID + "' class='hidden advancedAudioSettings'></div>";
	if (session.rpcs[UUID].pseudoguest){
		controls.querySelectorAll("[data-action-type]").forEach(ele => {
			ele.dataset.UUID = UUID;
			ele.dataset.sid = streamID;
		});
		return;
	}
	//controls.innerHTML += "<div id='advanced_video_director_" + UUID + "' class='hidden advancedVideoSettings'></div>";

	if (!session.rpcs[UUID].voiceMeter) {
		if (session.meterStyle == 1) {
			// director specific style
			session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate2").cloneNode(true);
		} else {
			session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate").cloneNode(true);
			session.rpcs[UUID].voiceMeter.style.opacity = 0;
			if (session.meterStyle == 2) {
				session.rpcs[UUID].voiceMeter.classList.add("video-meter-2");
				session.rpcs[UUID].voiceMeter.classList.remove("video-meter");
			} else {
				session.rpcs[UUID].voiceMeter.classList.add("video-meter-director");
			}
		}
		session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID;
		session.rpcs[UUID].voiceMeter.dataset.level = 0;
		session.rpcs[UUID].voiceMeter.classList.remove("hidden");
	}

	session.rpcs[UUID].remoteMuteElement = getById("muteStateTemplate").cloneNode(true);
	session.rpcs[UUID].remoteMuteElement.id = "";
	session.rpcs[UUID].remoteMuteElement.style.top = "5px";
	session.rpcs[UUID].remoteMuteElement.style.right = "7px";

	session.rpcs[UUID].remoteVideoMuteElement = getById("videoMuteStateTemplate").cloneNode(true);
	session.rpcs[UUID].remoteVideoMuteElement.id = "";
	session.rpcs[UUID].remoteVideoMuteElement.style.top = "5px";
	session.rpcs[UUID].remoteVideoMuteElement.style.right = "28px";
	
	session.rpcs[UUID].remoteRaisedHandElement = getById("raisedHandTemplate").cloneNode(true);
	session.rpcs[UUID].remoteRaisedHandElement.id = "";
	session.rpcs[UUID].remoteRaisedHandElement.style.top = "5px";
	session.rpcs[UUID].remoteRaisedHandElement.style.right = "49px";
	
	var handsID = "hands_" + UUID;

	// controls.innerHTML += "<div class='flexBreak'><span data-translate='links'>Links</span></div>"; //Seems to create an empty div.

	if (session.hidesololinks == false) {
		controls.innerHTML +=
			"<div class='soloButton' title='A direct solo view of the video/audio stream with nothing else. Its audio can be remotely controlled from here'> \
				<a class='soloLink advanced task'  data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \
				value='" +
			soloLink +
			"' href='" +
			soloLink +
			"'/>" +
			sanitizeChat(soloLink) +
			"</a>\
				<button class='pull-right controlsGrid' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span data-translate='copy-solo-view-link'>copy solo view link</span></button>\
			</div>";
	}

	controls.innerHTML +=
		'<button data-action-type="hand-raised" data--u-u-i-d=\'' +
		UUID +
		"' id='" +
		handsID +
		"' class='hidden lowerRaisedHand' title=\"This guest raised their hand. Click this to clear notification.\" onclick=\"remoteLowerhands('" +
		UUID +
		'\');">\
			<i class="las la-hand-paper"></i>\
			<span data-translate="user-raised-hand">Lower Raised Hand</span>\
		</button>\
		</div>';

	controls.innerHTML +=
		'<button data-action-type="remove-queue"  data--u-u-i-d=\'' +
		UUID +
		'\' class=\'hidden removeFromQueue\' title="Takes the guest out of queue mode; they will then join as a normal guest." onclick="remoteRemoveQueue(this);">\
			<i class="las la-plus"></i>\
			<span data-translate="remove-from-queue">Activate Guest</span>\
		</button>\
		</div>';

	controls.querySelectorAll("[data-action-type]").forEach(ele => {
		// give action buttons some self-reference
		ele.dataset.UUID = UUID;
		ele.dataset.sid = streamID;
	});
	var buttons = "";
	if (session.slotmode) {
	    var biggestSlot = 0;
	    var slotDefault = null;
	   
	    // Handle initial slot value from initialization or past slots
	    if (slot_init && session.slotmode == 1) {
		   slotDefault = slot_init || null;
	    }
	    if (streamID in session.pastSlots) {
		   slotDefault = session.pastSlots[streamID];
	    }
	   
	    // Get all current slots from RPC state
	    var allSlots = [];
		if (session.slotmode == 1) {
			Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => {
				if (currentSlot) {
					if (parseInt(currentSlot) > biggestSlot) {
						biggestSlot = parseInt(currentSlot);
					}
					if (slotDefault === parseInt(currentSlot)) {
						slotDefault = null;
					}
					allSlots.push(parseInt(currentSlot));
				}
			});
			biggestSlot += 1;
		} else if (slotDefault !== null && session.slotmode == 2) {
			// Check if slot is already in use by any remote participant
			const remoteSlotInUse = Object.keys(session.rpcs).some(UUID => 
				getSlotState(UUID) === slotDefault
			);
			
			// Check if slot is used by the director
			const directorSlotInUse = Object.entries(session.currentSlots).some(([slot, sid]) => 
				parseInt(slot) === slotDefault && (sid === session.streamID || sid === session.streamID+":s")
			);
			
			if (remoteSlotInUse || directorSlotInUse) {
				slotDefault = null; // This slot is already in use
			}
		}
	   
	   // Determine final slot value
	   if (slotDefault !== null) {
		   // the default slot is available
		   biggestSlot = slotDefault;
	   } else if (slot_init && session.slotmode == 1) {
		   // was manually set, so can't be something else but 0
		   biggestSlot = 0;
	   } else if (session.slotmode == 1) {
		   var bestfree = 0;
		   for (var i = 1; i <= biggestSlot; i++) {
			   if (allSlots.includes(i)) {
				   continue;
			   } else {
				   bestfree = i;
				   break;
			   }
		   }
		   biggestSlot = bestfree;
	   }
	   
	   // Set slot name
	   var slotName = biggestSlot ? "slot: " + biggestSlot : "unset";
	   
		buttons +=
			`<div draggable='true' title='Drag to swap layout positions' 
				 ondragend='dragendSlot(event)' 
				 ondragstart='dragSlot(event)' 
				 ondrop='dropSlot(event)' 
				 ondragover='allowDropSlot(event)' 
				 data-sid='${streamID}' 
				 data-uuid='${UUID}'
				 data-slot='${biggestSlot}' 
				 class='slotsbar'>
			   <button ondrop='dropSlot(event)' 
					   data-action-type='setslot' 
					   draggable='true' 
					   ondragend='dragendSlot(event)' 
					   ondragstart='dragSlot(event)' 
					   ondragover='allowDropSlot(event)' 
					   onclick='changeSlot(event, this);'>${slotName}</button>
			</div>`;

	   // Sync the initial state
	   syncSlotState(streamID, biggestSlot, false); // false since UI is being created here
	}
	buttons +=
		"<div title='Does not impact scene order.' class='shift'><i class='las la-angle-left' data--u-u-i-d='" +
		UUID +
		"' onclick='shiftPC(this,-1);'></i><span onclick='lockPosition(this);' style='cursor:pointer;' data-locked='0' data--u-u-i-d='" +
		UUID +
		"' id='position_" +
		UUID +
		"'><i class='las la-lock-open'></i></span><i class='las la-angle-right' data--u-u-i-d='" +
		UUID +
		"' onclick='shiftPC(this,1);'></i></div><div class='streamID' style='user-select: none;'>ID: <span style='user-select: text;'>" +
		streamID +
		"</span>\
	<i class='las la-copy' data-sid='" +
		streamID +
		"' onclick='copyFunction(this.dataset.sid,event)' title='Copy this Stream ID to the clipboard' style='cursor:pointer'></i>\
	<i class='las la-window-minimize' data--u-u-i-d='" +
		UUID +
		"' onclick='minimizeMe(this)' title='Minimize this control box' style='cursor:pointer'></i>\
	<span id='label_" +
		UUID +
		"' class='addALabel' title='Click here to edit the label for this stream. Changes will propagate to all viewers of this stream'></span>\
	</div>";

	container.innerHTML = buttons;
	updateLockedElements();

	var videoContainerControlBox = document.createElement("div");
	videoContainerControlBox.className = "controlVideoBox";
	container.containerControlBox = videoContainerControlBox;
	container.appendChild(videoContainerControlBox);

	var videoContainer = document.createElement("div");
	videoContainer.id = "videoContainer_" + UUID; // needed to delete on user disconnect
	videoContainer.style.margin = "0";
	videoContainer.style.position = "relative";
	videoContainer.style.minHeight = "30px";
	
	videoContainerControlBox.appendChild(videoContainer);

	if (session.signalMeter) {
		if (!session.rpcs[UUID].signalMeter) {
			session.rpcs[UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true);
			session.rpcs[UUID].signalMeter.id = "signalMeter_" + UUID;
			session.rpcs[UUID].signalMeter.dataset.level = 0;
			session.rpcs[UUID].signalMeter.classList.remove("hidden");
			session.rpcs[UUID].signalMeter.dataset.UUID = UUID;
			session.rpcs[UUID].signalMeter.title = getTranslation("signal-meter");

			if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.cpuLimited) {
				// was quality_limitation_reason
				session.rpcs[UUID].signalMeter.dataset.cpu = "1";
			}

			if (session.statsMenu !== false) {
				session.rpcs[UUID].signalMeter.addEventListener("click", function (e) {
					// show stats of video if double clicked
					log("clicked signal meter");
					try {
						e.preventDefault();
						if (session.statsMenu !== false) {
							var uid = e.currentTarget.dataset.UUID;
							if ("stats" in session.rpcs[uid]) {
								var [menu, innerMenu] = statsMenuCreator();
								printViewStats(innerMenu, uid);
								menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
							}
						}
						e.stopPropagation();
						return false;
					} catch (e) {
						errorlog(e);
					}
				});
			}
		}
		videoContainer.appendChild(session.rpcs[UUID].signalMeter);
	}

	if (session.batteryMeter) {
		if (!session.rpcs[UUID].batteryMeter) {
			session.rpcs[UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true);
			session.rpcs[UUID].batteryMeter.id = "batteryMeter_" + UUID;
			batteryMeterInfoUpdate(UUID);
		}
		videoContainer.appendChild(session.rpcs[UUID].batteryMeter);
	}

	if (session.showConnections) {
		if (!session.rpcs[UUID].connectionDetails) {
			createConnectionDetailsEle(UUID);
		}
		videoContainer.appendChild(session.rpcs[UUID].connectionDetails);
	}
	
	var iframeDetails = document.createElement("div");
	iframeDetails.id = "iframeDetails_" + UUID; // needed to delete on user disconnect
	iframeDetails.className = "iframeDetails hidden";

	videoContainer.appendChild(session.rpcs[UUID].voiceMeter);
	videoContainer.appendChild(session.rpcs[UUID].remoteMuteElement);
	videoContainer.appendChild(session.rpcs[UUID].remoteVideoMuteElement);
	videoContainer.appendChild(session.rpcs[UUID].remoteRaisedHandElement);
	videoContainer.appendChild(iframeDetails);
	container.appendChild(controls);
	
	session.group.forEach(group => {
		var ele = controls.querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]');
		if (!ele) {
			var newGroup = htmlToElement('<button style="margin: 0 5px 10px 5px;" data-sid="' + session.rpcs[UUID].streamID + '" data--u-u-i-d="' + UUID + '" data-action-type="toggle-group" data-group="' + group + '"   title="Add to Group: ' + group + '" onclick="changeGroup(this, event);"><span ><i class="las la-users" style="color:#060"></i>' + group + "</span></button>");

			var added = false;
			container.querySelectorAll(".customGroup>[data-group]").forEach(ele => {
				log(ele);
				if (!added && ele.dataset.group > group + "") {
					ele.parentNode.insertBefore(newGroup, ele);
					added = true;
				}
			});
			if (!added) {
				var newGroupCon = container.querySelector(".customGroup");
				if (!newGroupCon) {
					newGroupCon = document.createElement("div");
					newGroupCon.classList.add("customGroup");
					container.appendChild(newGroupCon);
				}
				newGroupCon.appendChild(newGroup);
			}
		}
	});
	

	initSceneList(UUID);
	syncSceneState(streamID);
	syncOtherState(streamID);

	pokeIframeAPI("control-box", true, UUID);
}


function createControlBoxScreenshare(UUID, soloLink, streamID) {
	if (document.getElementById("deleteme")) {
		getById("deleteme").parentNode.removeChild(getById("deleteme"));
	}
	var controls = getById("controls_blank").cloneNode(true);
	controls.classList.remove("hidden");
	controls.id = "controls_" + UUID;

	var container = document.createElement("div");
	container.className = "vidcon directorMargins";
	container.id = "container_" + UUID; // needed to delete on user disconnect
	container.UUID = UUID;
	container.dataset.UUID = UUID;
	container.dataset.sid = streamID;

	if (session.orderby) {
		try {
			var added = false;
			for (var i = 0; i < getById("guestFeeds").children.length; i++) {
				if (getById("guestFeeds").children[i].UUID && !getById("guestFeeds").children[i].shifted) {
					if (getById("guestFeeds").children[i].UUID in session.rpcs) {
						if (session.rpcs[getById("guestFeeds").children[i].UUID].streamID.toLowerCase() > streamID.toLowerCase()) {
							getById("guestFeeds").insertBefore(container, getById("guestFeeds").children[i]);
							added = true;
							break;
						}
					}
				}
			}
			if (!added) {
				getById("guestFeeds").appendChild(container);
			}
		} catch (e) {
			getById("guestFeeds").appendChild(container);
		}
	} else {
		getById("guestFeeds").appendChild(container);
	}

	controls.querySelector(".controlsGrid").classList.add("notmain");

	if (!session.rpcs[UUID].voiceMeter) {
		if (session.meterStyle == 1) {
			session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate2").cloneNode(true);
		} else {
			session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate").cloneNode(true);
			session.rpcs[UUID].voiceMeter.style.opacity = 0;
			if (session.meterStyle == 2) {
				session.rpcs[UUID].voiceMeter.classList.add("video-meter-2");
				session.rpcs[UUID].voiceMeter.classList.remove("video-meter");
			} else {
				session.rpcs[UUID].voiceMeter.classList.add("video-meter-director");
			}
		}
		session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID;
		session.rpcs[UUID].voiceMeter.dataset.level = 0;
		session.rpcs[UUID].voiceMeter.classList.remove("hidden");
	}

	session.rpcs[UUID].remoteMuteElement = getById("muteStateTemplate").cloneNode(true);
	session.rpcs[UUID].remoteMuteElement.id = "";
	session.rpcs[UUID].remoteMuteElement.style.top = "5px";
	session.rpcs[UUID].remoteMuteElement.style.right = "7px";

	session.rpcs[UUID].remoteVideoMuteElement = getById("videoMuteStateTemplate").cloneNode(true);
	session.rpcs[UUID].remoteVideoMuteElement.id = "";
	session.rpcs[UUID].remoteVideoMuteElement.style.top = "5px";
	session.rpcs[UUID].remoteVideoMuteElement.style.right = "28px";

	session.rpcs[UUID].remoteRaisedHandElement = getById("raisedHandTemplate").cloneNode(true);
	session.rpcs[UUID].remoteRaisedHandElement.id = "";
	session.rpcs[UUID].remoteRaisedHandElement.style.top = "5px";
	session.rpcs[UUID].remoteRaisedHandElement.style.right = "49px";

	var videoContainer = document.createElement("div");
	videoContainer.id = "videoContainer_" + UUID; // needed to delete on user disconnect
	videoContainer.style.margin = "0";
	videoContainer.style.position = "relative";
	videoContainer.style.minHeight = "30px";

	var iframeDetails = document.createElement("div");
	iframeDetails.id = "iframeDetails_" + UUID; // needed to delete on user disconnect
	iframeDetails.className = "iframeDetails hidden";

	//controls.innerHTML += "<div id='advanced_audio_director_" + UUID + "' class='hidden advancedAudioSettings'></div>";
	//controls.innerHTML += "<div id='advanced_video_director_" + UUID + "' class='hidden advancedVideoSettings'></div>";

	var handsID = "hands_" + UUID;

	controls.innerHTML += "<div>";

	if (session.hidesololinks == false) {
		controls.innerHTML +=
			"<div class='soloButton' title='A direct solo view of the video/audio stream with nothing else.'> \
			<a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \
			value='" +
			soloLink +
			"' href='" +
			soloLink +
			"'/>" +
			sanitizeChat(soloLink) +
			"</a>\
			<button class='pull-right' style='width:100%;' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span data-translate='copy-solo-view-link'>copy solo view link</span></button>\
			</div>";
	}

	controls.innerHTML +=
		'<button data-action-type="hand-raised" id=\'' +
		handsID +
		"' class='hidden lowerRaisedHand' title=\"This guest raised their hand. Click this to clear notification.\" onclick=\"remoteLowerhands('" +
		UUID +
		'\');">\
			<i class="las la-hand-paper"></i>\
			<span data-translate="user-raised-hand">Lower Raised Hand</span>\
		</button>\
		</div>';

	controls.querySelectorAll("[data-action-type]").forEach(ele => {
		// give action buttons some self-reference
		ele.dataset.UUID = UUID;
		ele.dataset.sid = streamID;
	});
	
	var buttons = "";
	if (session.slotmode) {
	   var biggestSlot = 0;
	   var slotDefault = null;
	   
	   // Check past slots first
	   if (streamID in session.pastSlots) {
		   slotDefault = session.pastSlots[streamID]; 
	   }
	   
	   // Get all current slots from RPC state
	   var allSlots = [];
	   if (session.slotmode == 1) {
		    Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => {
		  	    if (currentSlot) {
				   if (parseInt(currentSlot) > biggestSlot) {
					   biggestSlot = parseInt(currentSlot);
				   }
				   if (slotDefault === parseInt(currentSlot)) {
					   slotDefault = null;
				   }
				   allSlots.push(parseInt(currentSlot));
			    }
		    });
		    biggestSlot += 1;
	    }
	   
	   // Determine final slot value
	   if (slotDefault !== null) {
		   biggestSlot = slotDefault;
	   } else if (session.slotmode == 1) {
		   var bestfree = 0;
		   for (var i = 1; i <= biggestSlot; i++) {
			   if (allSlots.includes(i)) {
				   continue;
			   } else {
				   bestfree = i;
				   break;
			   }
		   }
		   biggestSlot = bestfree;
	   }
	   
	   // Set slot name and update past slots
	   var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; 
	   session.pastSlots[streamID] = biggestSlot;
	   
	   // Build HTML with same structure
	   buttons +=
		   `<div draggable='true' title='Drag to swap layout positions' 
				 ondragend='dragendSlot(event)' 
				 ondragstart='dragSlot(event)' 
				 ondrop='dropSlot(event)' 
				 ondragover='allowDropSlot(event)' 
				 data-sid='${streamID}' 
				 data-uuid='${UUID}'
				 data-slot='${biggestSlot}' 
				 class='slotsbar'>
			   <button ondrop='dropSlot(event)' 
					   data-action-type='setslot' 
					   draggable='true' 
					   ondragend='dragendSlot(event)' 
					   ondragstart='dragSlot(event)' 
					   ondragover='allowDropSlot(event)' 
					   onclick='changeSlot(event, this);'>${slotName}</button>
			</div>`;

	   // Sync the initial state
	   syncSlotState(streamID, biggestSlot, false); // false since UI is being created here
	}

	buttons +=
		"<div title='Does not impact scene order.' class='shift'><i class='las la-angle-left' data--u-u-i-d='" +
		UUID +
		"' onclick='shiftPC(this,-1);'></i><span onclick='lockPosition(this);' style='cursor:pointer;' data-locked='0' data--u-u-i-d='" +
		UUID +
		"' id='position_" +
		UUID +
		"'><i class='las la-lock-open'></i></span><i class='las la-angle-right' data--u-u-i-d='" +
		UUID +
		"' onclick='shiftPC(this,1);'></i></div><div class='streamID' style='user-select: none;'>ID: <span style='user-select: text;'>" +
		streamID +
		"</span>\
	<i class='las la-copy' data-sid='" +
		streamID +
		"' onclick='copyFunction(this.dataset.sid,event)' title='Copy this Stream ID to the clipboard' style='cursor:pointer'></i>\
	<i class='las la-window-minimize' data--u-u-i-d='" +
		UUID +
		"' onclick='minimizeMe(this)' title='Minimize this control box' style='cursor:pointer'></i>\
	<span id='label_" +
		UUID +
		"' class='addALabel' title='Click here to edit the label for this stream. Changes will propagate to all viewers of this stream'></span>\
	</div>";

	container.innerHTML = buttons;
	updateLockedElements();

	var videoContainerControlBox = document.createElement("div");
	videoContainerControlBox.className = "controlVideoBox";
	container.containerControlBox = videoContainerControlBox;

	container.appendChild(videoContainerControlBox);
	videoContainerControlBox.appendChild(videoContainer);

	if (session.signalMeter) {
		if (!session.rpcs[UUID].signalMeter) {
			session.rpcs[UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true);
			session.rpcs[UUID].signalMeter.id = "signalMeter_" + UUID;
			session.rpcs[UUID].signalMeter.dataset.level = 0;
			session.rpcs[UUID].signalMeter.classList.remove("hidden");
			session.rpcs[UUID].signalMeter.dataset.UUID = UUID;
			session.rpcs[UUID].signalMeter.title = getTranslation("signal-meter");

			//if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.cpu_maxed){
			//	session.rpcs[UUID].signalMeter.dataset.cpu = "1";
			//}

			session.rpcs[UUID].signalMeter.addEventListener("click", function (e) {
				// show stats of video if double clicked
				log("clicked signal meter");
				try {
					e.preventDefault();
					if (session.statsMenu !== false) {
						var uid = e.currentTarget.dataset.UUID;
						if ("stats" in session.rpcs[uid]) {
							var [menu, innerMenu] = statsMenuCreator();
							printViewStats(innerMenu, uid);
							menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
						}
					}
					e.stopPropagation();
					return false;
				} catch (e) {
					errorlog(e);
				}
			});
		}
		videoContainer.appendChild(session.rpcs[UUID].signalMeter);
	}

	if (session.batteryMeter) {
		////////
		if (!session.rpcs[UUID].batteryMeter) {
			session.rpcs[UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true);
			session.rpcs[UUID].batteryMeter.id = "batteryMeter_" + UUID;
			batteryMeterInfoUpdate(UUID);
		}
		videoContainer.appendChild(session.rpcs[UUID].batteryMeter);
	}

	if (session.showConnections) {
		if (!session.rpcs[UUID].connectionDetails) {
			createConnectionDetailsEle(UUID);
		}
		videoContainer.appendChild(session.rpcs[UUID].connectionDetails);
	}

	videoContainer.appendChild(session.rpcs[UUID].voiceMeter);
	videoContainer.appendChild(session.rpcs[UUID].remoteMuteElement);
	videoContainer.appendChild(session.rpcs[UUID].remoteVideoMuteElement);
	videoContainer.appendChild(session.rpcs[UUID].remoteRaisedHandElement);
	videoContainer.appendChild(iframeDetails);
	videoContainer.appendChild(session.rpcs[UUID].videoElement);
	container.appendChild(controls);

	session.group.forEach(group => {
		var ele = controls.querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]');
		if (!ele) {
			var newGroup = htmlToElement('<button style="margin: 0 5px 10px 5px;" data-sid="' + session.rpcs[UUID].streamID + '" data--u-u-i-d="' + UUID + '" data-action-type="toggle-group" data-group="' + group + '" title="Add to Group: ' + group + '" onclick="changeGroup(this, event);"><span ><i class="las la-users" style="color:#060"></i>' + group + "</span></button>");

			var added = false;
			container.querySelectorAll(".customGroup>[data-group]").forEach(ele => {
				log(ele);
				if (!added && ele.dataset.group > group + "") {
					ele.parentNode.insertBefore(newGroup, ele);
					added = true;
				}
			});
			if (!added) {
				var newGroupCon = container.querySelector(".customGroup");
				if (!newGroupCon) {
					newGroupCon = document.createElement("div");
					newGroupCon.classList.add("customGroup");
					container.appendChild(newGroupCon);
				}
				newGroupCon.appendChild(newGroup);
			}
		}
	});

	initSceneList(UUID);
	pokeIframeAPI("control-box", true, UUID);
}

function remoteRemoveQueue(ele) {
	let ts = { ...transferSettings };
	ts.justResetting = true;
	session.directMigrateIssue(session.roomid, ts, ele.dataset.UUID);

	ele.classList.add("hidden");
}
function minimizeMe(button, director = false) {
	if (!director) {
		getById("container_" + button.dataset.UUID).classList.toggle("minimized");
	} else {
		getById(director).classList.toggle("minimized");
	}
}

function blackoutMode() {
    var overlay = document.getElementById("blackoutOverlay");
    if (!overlay) {
        overlay = document.createElement('div');
        overlay.id = "blackoutOverlay";
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: black;
            color: white;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 14px;
            cursor: pointer;
            z-index: 9999;
        `;
        overlay.textContent = 'Click to exit black-out mode';
        document.body.appendChild(overlay);
    } else {
        overlay.classList.remove("hidden");
    }
    
    function exitBlackout() {
        overlay.classList.add("hidden");
        overlay.removeEventListener('click', exitBlackout);
    }
    
    overlay.addEventListener('click', exitBlackout);
}


function cycleCameras() {
	if (session.screenShareState) {
		warnUser("Stop the screen-share first.");
		return;
	}
	var videoSelect = document.querySelector("select#videoSource3").options;
	// don't show flip option if only one camera.
	// don't show if not a mobile device
	// don't show if AD=0

	var matched = false;
	var maxIndex = parseInt(getById("flipcamerabutton").dataset.maxIndex) || parseInt(videoSelect.length);
	if (maxIndex > parseInt(videoSelect.length)) {
		maxIndex = parseInt(videoSelect.length);
	}

	for (var i = 0; i < maxIndex; i++) {
		var selOption = videoSelect[i];
		if (selOption.selected) {
			matched = true;
		} else if (matched) {
			if (getById("flipcamerabutton").classList.contains("flip")) {
				getById("flipcamerabutton").classList.remove("flip");
				getById("flipcamerabutton").classList.add("flip2");
			} else {
				getById("flipcamerabutton").classList.remove("flip2");
				getById("flipcamerabutton").classList.add("flip");
			}
			document.querySelector("select#videoSource3").value = selOption.value;
			activatedPreview = false;
			grabVideo(session.quality, "videosource", "select#videoSource3");
			return;
		}
	}
	for (var i = 0; i < maxIndex; i++) {
		var selOption = videoSelect[i];
		if (selOption.selected) {
			return; // do nothing; the camera that is selected is the only camera available it seems.
		} else {
			if (getById("flipcamerabutton").classList.contains("flip")) {
				getById("flipcamerabutton").classList.remove("flip");
				getById("flipcamerabutton").classList.add("flip2");
			} else {
				getById("flipcamerabutton").classList.remove("flip2");
				getById("flipcamerabutton").classList.add("flip");
			}
			document.querySelector("select#videoSource3").value = selOption.value;
			activatedPreview = false;
			grabVideo(session.quality, "videosource", "select#videoSource3");
			return;
		}
	}
}

function addToGoogleCalendar() {
	var title = "Live Stream";
	//var dates = "20180512T230000Z/20180513T030000Z";
	var linkout = getById("director_block_1").innerText;
	var details = "Join the live stream as a performer at the following link:<br/><br/>===>   " + linkout + "<br/><br/>To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest<br/><br/>Do not share the details of this invite with others, unless explicitly told to.";
	details = details.split(" ").join("+");
	details = details.split("&").join("%26");
	var linkToOpen = "https://calendar.google.com/calendar/r/eventedit?text=" + title + "&details=" + details;
	//https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134

	window.open(linkToOpen);
}

function addToOutlookCalendar() {
	var title = "Live Stream";
	var linkout = getById("director_block_1").innerText;
	var details = "Join the live stream as a performer at the following link:<br/><br/>===>   " + linkout + "<br/><br/>To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest<br/><br/>Do not share the details of this invite with others, unless explicitly told to.";
	details = details.split(" ").join("%20");
	details = details.split("&").join("%26");

	var linkToOpen = "https://outlook.live.com/owa/?path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&subject=" + title + "&body=" + details;
	//https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134

	window.open(linkToOpen);
}

function addToYahooCalendar() {
	var title = "Live Stream";
	var linkout = getById("director_block_1").innerText;
	var details = "Join the live stream as a performer at the following link:<br/><br/>===>   " + linkout + "<br/><br/>To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest<br/><br/>Do not share the details of this invite with others, unless explicitly told to.";
	details = details.split(" ").join("%20");
	details = details.split("&").join("%26");
	var linkToOpen = "https://calendar.yahoo.com?v60&title=" + title + "&desc=" + details;
	//https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134

	window.open(linkToOpen);
}

function toggle(ele, tog = false, inline = true) {
	var x = ele;
	if (x.style.display === "none") {
		if (inline) {
			x.style.display = "inline-block";
		} else {
			x.style.display = "block";
		}
	} else {
		x.style.display = "none";
	}
	if (tog) {
		if (tog.dataset.saved) {
			tog.innerHTML = tog.dataset.saved;
			delete tog.dataset.saved;
		} else {
			tog.dataset.saved = tog.innerHTML;
			tog.innerHTML = "Hide This";
		}
	}
}

function toggleByDataset(filter) {
	var elements = document.querySelectorAll('[data-cluster="' + filter + '"]'); // ie:  .cluster1
	for (var i = 0; i < elements.length; i++) {
		elements[i].classList.toggle("hidden");
	}
}

var SelectedAudioOutputDevices = false; // session.sink
var SelectedAudioInputDevices = []; // ..
var SelectedVideoInputDevices = []; // ..

async function enumerateDevices() {
    log("enumerated start");

    const timeout = new Promise((_, reject) => 
        setTimeout(() => reject(function(){
			if (!session.cleanOutput) {
				warnUser("The browser has not responded to our request to list available media devices.\n\nPossible solutions:\n\n- Restart the computer and try again\n- Try another browser\n- Remove or uninstall devices that are not needed\n- Uninstall and reinstall your browser");
			}
			new Error("Device enumeration timed out.\n\nThe browser has not responded to our request to list available media devices.\n\nPossible solutions:\n\n- Restart the computer and try again\n- Try another browser\n- Remove or uninstall devices that are not needed\n- Uninstall and reinstall your browser");
		}), 15000)
    );

    const enumeratePromise = new Promise(async (resolve, reject) => {
        try {
            if (typeof navigator.mediaDevices === "object" && typeof navigator.mediaDevices.enumerateDevices === "function") {
                resolve(await navigator.mediaDevices.enumerateDevices());
            } else if (typeof navigator.enumerateDevices === "function") {
                log("enumerated failed 1");
                resolve(await navigator.enumerateDevices());
            } else {
                window.MediaStreamTrack.getSources(devices => {
                    resolve(
                        devices
                            .filter(device => {
                                return device.kind.toLowerCase() === "video" || device.kind.toLowerCase() === "videoinput";
                            })
                            .map(device => {
                                return {
                                    deviceId: device.deviceId != null ? device.deviceId : "",
                                    groupId: device.groupId,
                                    kind: "videoinput",
                                    label: device.label,
                                    toJSON: /* istanbul ignore next */ function () {
                                        return this;
                                    }
                                };
                            })
                    );
                });
            }
        } catch (e) {
            errorlog(e);
            if (!session.cleanOutput) {
                if (location.protocol !== "https:") {
                    warnUser("Error listing the media devices.\n\nYour browser will not allow access to media devices without SSL enabled.\n\nPossible solutions include switching to https, accessing the site from http://localhost, or enabling the `unsafely-treat-insecure-origin-as-secure` browser switch.");
                } else if ("isSecureContext" in window && window.isSecureContext === false) {
                    warnUser("Error listing the media devices.\n\nThe website may have assets loaded in an insecure context.");
                } else {
                    warnUser("An unknown error occured while trying to list the media devices.");
                }
            }
            reject(e);
        }
    });

    return Promise.race([enumeratePromise, timeout]);
}

function requestOutputAudioStream() {
	try {
		//warnlog("GET USER MEDIA");
		warnlog("navigator.mediaDevices.getUserMedia starting...");
		return navigator.mediaDevices
			.getUserMedia({
				audio: true,
				video: false
			})
			.then(function (stream1) {
				// Apple needs thi to happen before I can access EnumerateDevices.
				log("get media sources; request audio stream");
				return enumerateDevices().then(function (deviceInfos) {
					stream1.getTracks().forEach(function (track) {
						// We don't want to keep it without audio; so we are going to try to add audio now.
						track.stop(); // I need to do this after the enumeration step, else it breaks firefox's labels
					});
					const audioOutputSelect = getById("outputSourceScreenshare");
					audioOutputSelect.remove(0);
					audioOutputSelect.removeAttribute("onclick");

					for (let i = 0; i !== deviceInfos.length; ++i) {
						const deviceInfo = deviceInfos[i];
						if (deviceInfo == null) {
							continue;
						}
						const option = document.createElement("option");
						option.value = deviceInfo.deviceId;
						if (deviceInfo.kind === "audiooutput") {
							const option = document.createElement("option");
							if (audioOutputSelect.length === 0) {
								option.dataset.default = true;
							} else {
								option.dataset.default = false;
							}
							option.value = deviceInfo.deviceId || "default";
							if (option.value == session.sink) {
								option.selected = "true";
							}
							option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`;
							audioOutputSelect.appendChild(option);
						} else {
							log("Some other kind of source/device: ", deviceInfo);
						}
					}
				});
			});
	} catch (e) {
		if (!session.cleanOutput) {
			if (window.isSecureContext) {
				warnUser("An error has occured when trying to access the default audio device. The reason is not known.");
			} else if (iOS || iPad) {
				warnUser("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported.");
			} else {
				warnUser("Error accessing the default audio device.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia");
			}
		}
	}
}

let selectedScreenShareAudioDevices = [];
async function requestAudioStream() { // for the screen share.
    const deviceList = document.getElementById('audioDeviceList');
    const errorElement = document.getElementById('audioSelectError');
    const showDevicesButton = document.getElementById('showAudioDevices');

    try {
        // Request audio permission first
        const stream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: false
        });
        
        // Stop tracks after getting permission
        stream.getTracks().forEach(track => track.stop());
        
        // Enumerate devices
        const devices = await navigator.mediaDevices.enumerateDevices();
        const audioInputs = devices.filter(device => device.kind === 'audioinput');

        // Clear and show device list
        deviceList.innerHTML = '';
        deviceList.style.display = 'block';
        showDevicesButton.style.display = 'none';

		selectedScreenShareAudioDevices = [];
		
        // Create checkbox for each device
        audioInputs.forEach(device => {
            const deviceLabel = device.label || `Microphone ${device.deviceId.slice(0, 4)}`;
            
            const div = document.createElement('div');
            div.className = 'audio-device-item';
            
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.id = device.deviceId;
            checkbox.value = device.deviceId;
            
            // Check if device was previously selected
            if (session.audioDevice && ((typeof session.audioDevice === 'object' && session.audioDevice.includes(device.deviceId)) ||  normalizeDeviceLabel(deviceLabel).includes(session.audioDevice))) {
                checkbox.checked = true;
				selectedScreenShareAudioDevices.push(device.deviceId);
            }

            checkbox.addEventListener('change', function() {
                // Update session.audioDevice array
               // if (!session.audioDevice || typeof session.audioDevice !== 'object') {
                   // session.audioDevice = [];
               // }
				if (!selectedScreenShareAudioDevices || typeof selectedScreenShareAudioDevices !== 'object') {
                    selectedScreenShareAudioDevices = [];
                }
                
                if (this.checked) {
                   // if (!session.audioDevice.includes(this.value)) {
                      //  session.audioDevice.push(this.value);
                   // }
					if (!selectedScreenShareAudioDevices.includes(this.value)) {
						selectedScreenShareAudioDevices.push(this.value);
                    }
                } else {
                    //session.audioDevice = session.audioDevice.filter(id => id !== this.value);
					selectedScreenShareAudioDevices = selectedScreenShareAudioDevices.filter(id => id !== this.value);
                }
            });

            const label = document.createElement('label');
            label.htmlFor = device.deviceId;
            label.textContent = deviceLabel;

            div.appendChild(checkbox);
            div.appendChild(label);
            deviceList.appendChild(div);
        });

        errorElement.style.display = 'none';

    } catch (e) {
        let errorMessage = '';
        
        if (!window.isSecureContext) {
            errorMessage = 'This website must be loaded in a secure context (HTTPS) to access audio devices.';
        } else if (/ipad|iphone|ipod/.test(navigator.userAgent.toLowerCase())) {
            errorMessage = 'iOS 13.4 or later is recommended for audio device access.';
        } else {
            errorMessage = 'An error occurred while accessing audio devices.';
        }
        
        errorElement.textContent = errorMessage;
        errorElement.style.display = 'block';
    }
}


function saveSettings() {
	if (session.store) {
		try {
			var tmp = {};
			if (SelectedAudioInputDevices) {
				tmp.SelectedAudioInputDevices = SelectedAudioInputDevices.filter(n => n);
			}
			if (session.sink && session.sink != "default") {
				tmp.SelectedAudioOutputDevices = session.sink;
			} else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices != "default") {
				tmp.SelectedAudioOutputDevices = SelectedAudioOutputDevices;
			}
			tmp.SelectedVideoInputDevices = SelectedVideoInputDevices;
			setStorage("session_store", JSON.stringify(tmp));
			log("Saving settings");
		} catch (e) {
			errorlog(e);
		}
	}
}

function loadSettings() {
	if (session.store) {
		try {
			session.store = getStorage("session_store");
			if (session.store) {
				session.store = JSON.parse(session.store);
			} else {
				session.store = {};
			}

			if (session.store && session.store.SelectedAudioOutputDevices) {
				if (typeof session.store.SelectedAudioOutputDevices == "string") {
					SelectedAudioOutputDevices = session.store.SelectedAudioOutputDevices;
				} else if (typeof session.store.SelectedAudioOutputDevices == "object") {
					if (session.store.SelectedAudioOutputDevices.length) {
						SelectedAudioOutputDevices = session.store.SelectedAudioOutputDevices[0];
					}
				}
			}
			if (session.store && session.store.SelectedAudioInputDevices) {
				session.store.SelectedAudioInputDevices = session.store.SelectedAudioInputDevices.filter(n => n);
				SelectedAudioInputDevices = session.store.SelectedAudioInputDevices;
			}
			if (session.store && session.store.SelectedVideoInputDevices) {
				SelectedVideoInputDevices = session.store.SelectedVideoInputDevices;
			}
		} catch (e) {}
	}
}

function normalizeDeviceLabel(deviceName) {
	return String(deviceName).replace(/[\W]+/g, "_").toLowerCase();
} 

function gotDevices(deviceInfos, miconly = false) {
	log("got devices!1");
	log(deviceInfos);
	
	deviceInfos.sort((a, b) => {
		// Put "default" devices first
		if (a.deviceId.toLowerCase() === "default") return -1;
		if (b.deviceId.toLowerCase() === "default") return 1;
		
		// Then sort by label if both exist
		if (a.label && b.label) {
			return a.label.localeCompare(b.label, undefined, {sensitivity: 'base'});
		}
		
		return 0;
	});
	
	try {
		if (Firefox && !FirefoxEnumerated) {
			if (session.streamSrc && session.streamSrc.getTracks().length) {
				FirefoxEnumerated = true;
			}
		}

		var option = document.createElement("input");
		option.type = "checkbox";
		option.value = "ZZZ";
		option.name = "multiselect1";
		option.id = "multiselect1";
		option.style.display = "none";
		option.checked = true;

		var label = document.createElement("label");
		label.for = option.name;
		label.innerHTML = '<span data-translate="no-audio"> No Audio</span>';

		var listele = document.createElement("li");
		listele.appendChild(option);
		listele.appendChild(label);

		const audioInputSelect = document.getElementById("audioSource") || document.getElementById("audioSource3");
		audioInputSelect.innerHTML = "";
		audioInputSelect.appendChild(listele);

		const audioOutputSelect = document.getElementById("outputSource") || document.getElementById("outputSource3");
		audioOutputSelect.innerHTML = "";

		option.onchange = function (event) {
			// make sure to clear 'no audio option' if anything else is selected
			if (!getById("multiselect1").checked) {
				getById("multiselect1").checked = true;
			} else {
				var list = audioInputSelect.querySelectorAll("li>input");
				for (var i = 0; i < list.length; i++) {
					if (list[i].id !== "multiselect1") {
						list[i].checked = false;
					}
				}
			}
			SelectedAudioInputDevices = [event.currentTarget.value];
			saveSettings();
		};

		const multiselectTrigger = document.getElementById("multiselect-trigger") || document.getElementById("multiselect-trigger3");
		multiselectTrigger.dataset.state = "0";
		multiselectTrigger.classList.add("closed");
		multiselectTrigger.classList.remove("open");
		getById("chevarrow1").classList.add("bottom");

		const videoSelect = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3");
		const selectors = [videoSelect];

		const values = selectors.map(select => select.value);
		selectors.forEach(select => {
			while (select.firstChild) {
				select.removeChild(select.firstChild);
			}
		});

		function comp(a, b) {
			if (a.kind === "audioinput") {
				return 0;
			} else if (a.kind === "audiooutput") {
				return 0;
			}
			const labelA = a.label.toUpperCase();
			const labelB = b.label.toUpperCase();
			if (labelA > labelB) {
				return 1;
			} else if (labelA < labelB) {
				return -1;
			}
			return 0;
		}
		//deviceInfos.sort(comp); // I like this idea, but it messes with the defaults.  I just don't know what it will do.
		var deviceInfo;

		// This is to hide NDI from default device. NDI Tools fucks up.
		var tmp = [];
		for (let i = 0; i !== deviceInfos.length; ++i) {
			deviceInfo = deviceInfos[i];
			if (!(deviceInfo.kind === "videoinput" && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek")))) {
				tmp.push(deviceInfo);
			}
		}

		for (let i = 0; i !== deviceInfos.length; ++i) {
			deviceInfo = deviceInfos[i];
			if (deviceInfo.kind === "videoinput" && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek"))) {
				tmp.push(deviceInfo);
				log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
			}
		}
		deviceInfos = tmp;

		if (typeof session.audioDevice == "object") {
			// this sorts according to users's manual selection
			var matched1 = [];
			var matched2 = [];
			var notmatched = [];
			for (let i = 0; i !== deviceInfos.length; ++i) {
				if (deviceInfos[i].kind === "audioinput") {
					if (session.audioDevice.includes(deviceInfos[i].deviceId)) {
						matched1.push(deviceInfos[i]);
					} else if (session.audioDevice.includes(normalizeDeviceLabel(deviceInfos[i].label))) {
						matched1.push(deviceInfos[i]);
					} else {
						for (var j = 0; j < session.audioDevice.length; j++) {
							if (normalizeDeviceLabel(deviceInfos[i].label).includes(session.audioDevice[j])) {
								matched2.push(deviceInfos[i]);
								log("A DEVICE FOUND = " + deviceInfos[i].label);
								break;
							}
						}
					}
				} else {
					notmatched.push(deviceInfos[i]);
				}
			}
			
			matched2.sort((a, b) => {
				if (a.label && b.label) {
					if (a.label.length < b.label.length){
						return -1
					}
					return a.label.localeCompare(b.label, undefined, {sensitivity: 'base'});
				}
				return 0;
			});
			
			var matched = matched1.concat(matched2);
			deviceInfos = matched.concat(notmatched);
		} else if (session.store && session.store.SelectedAudioInputDevices) {
			var matched = [];
			var notmatch = [];
			for (let i = 0; i < deviceInfos.length; ++i) {
				deviceInfo = deviceInfos[i];
				if (session.store.SelectedAudioInputDevices.includes(deviceInfo.deviceId)) {
					matched.push(deviceInfo);
					log("EXACT A DEVICE FOUND -- from saved session");
				} else {
					notmatch.push(deviceInfo);
				}
			}
			deviceInfos = matched.concat(notmatch);
		}

		if (session.sink || SelectedAudioOutputDevices) {
			// this sorts according to users's manual selection
			var matched = [];
			var notmatch = [];
			for (let i = 0; i !== deviceInfos.length; ++i) {
				deviceInfo = deviceInfos[i];
				if (deviceInfo.kind === "audiooutput" && deviceInfo.deviceId === session.sink) {
					matched.push(deviceInfo);
				} else if (!session.sink && deviceInfo.kind === "audiooutput" && deviceInfo.deviceId === SelectedAudioOutputDevices) {
					matched.push(deviceInfo);
				} else {
					notmatch.push(deviceInfo);
				}
			}
			deviceInfos = matched.concat(notmatch);
		}

		if (session.videoDevice && session.videoDevice !== 1) {
			var tmp = [];
			var tmp2 = [];
			var tmp3 = [];
			var deviceIdMatch = false;

			// First pass - check for label matches
			for (let i = 0; i !== deviceInfos.length; ++i) {
				deviceInfo = deviceInfos[i];
				if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).startsWith(session.videoDevice)) {
					tmp.push(deviceInfo);
					log("Starts With V DEVICE FOUND");
				} else if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice)) {
					tmp2.push(deviceInfo);
					log("Includes With V DEVICE FOUND");
				} else {
					tmp3.push(deviceInfo);
				}
			}

			// If no label matches found, try device ID match
			if (tmp.length === 0 && tmp2.length === 0) {
				for (let i = 0; i < tmp3.length; ++i) {
					deviceInfo = tmp3[i];
					if (deviceInfo.kind === "videoinput" && deviceInfo.deviceId === session.videoDevice) {
						tmp.push(deviceInfo);
						deviceIdMatch = true;
						log("EXACT DEVICE ID MATCH FOUND");
						break;
					}
				}
			}

			if (tmp2.length && !deviceIdMatch) {
				tmp = tmp.concat(tmp2);
			}
			if (tmp3.length) {
				tmp = tmp.concat(tmp3);
			}

			deviceInfos = tmp;
			log("VDEVICE:" + session.videoDevice);
			log(deviceInfos);
		} else if (session.videoDevice === false && session.facingMode) {
			var tmp = [];
			if (session.facingMode == "environment") {
				for (let i = 0; i !== deviceInfos.length; ++i) {
					deviceInfo = deviceInfos[i];
					if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("back")) {
						tmp.push(deviceInfo);
						log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
					} else if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("rear")) {
						tmp.push(deviceInfo);
						log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
					}
				}
			} else if (session.facingMode == "user") {
				for (let i = 0; i !== deviceInfos.length; ++i) {
					deviceInfo = deviceInfos[i];
					if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("front")) {
						tmp.push(deviceInfo);
						log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
					}
				}
			}
			for (let i = 0; i !== deviceInfos.length; ++i) {
				deviceInfo = deviceInfos[i];
				if (!(deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice))) {
					if (deviceInfo.deviceId !== session.videoDevice) {
						tmp.push(deviceInfo);
					}
				}
			}
			deviceInfos = tmp;
			log("VDECICE:" + session.videoDevice);
			log(deviceInfos);
		} else if (session.store && session.store.SelectedVideoInputDevices && session.videoDevice === false) {
			var matched = [];
			var notmatch = [];
			for (let i = 0; i !== deviceInfos.length; ++i) {
				deviceInfo = deviceInfos[i];
				if (session.store.SelectedVideoInputDevices.includes(deviceInfo.deviceId)) {
					matched.push(deviceInfo);
					log("EXACT V DEVICE FOUND -- from saved session");
				} else {
					notmatch.push(deviceInfo);
				}
			}
			deviceInfos = matched.concat(notmatch);
			delete session.store.SelectedVideoInputDevices;
		} else if (session.mobile){
			var tmp = [];
			
			for (let i = 0; i !== deviceInfos.length; ++i) {
				deviceInfo = deviceInfos[i];
				if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("front")) {
					tmp.push(deviceInfo);
					log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
				}
			}
		
			for (let i = 0; i !== deviceInfos.length; ++i) {
				deviceInfo = deviceInfos[i];
				if (!(deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice))) {
					if (deviceInfo.deviceId !== session.videoDevice) {
						tmp.push(deviceInfo);
					}
				}
			}
			deviceInfos = tmp;
			log("AUTO FRONT:" + session.videoDevice);
			log(deviceInfos);
		}

		if (session.audioDevice && typeof session.audioDevice == "object") {
			var adMatch = [...session.audioDevice];
		} else if (session.store && session.store.SelectedAudioInputDevices && session.store.SelectedAudioInputDevices.length) {
			var adMatch = [...session.store.SelectedAudioInputDevices];
		} else {
			var adMatch = false;
		}

		if (session.store && session.store.SelectedAudioInputDevices) {
			delete session.store.SelectedAudioInputDevices;
		}

		var counter = 1;
		for (let i = 0; i !== deviceInfos.length; ++i) {
			var deviceInfo = deviceInfos[i];
			if (deviceInfo == null) {
				continue;
			}

			if (deviceInfo.kind === "audioinput") {
				option = document.createElement("input");
				option.type = "checkbox";
				counter++;
				listele = document.createElement("li");
				listele.style.display = "none";

				if (typeof adMatch == "object") {
					for (var j = 0; j < adMatch.length; j++) {
						if (!adMatch[j]) {
							// skip, already matched
						} else if (adMatch[j] == deviceInfo.deviceId) {
							option.checked = true;
							listele.style.display = "block";
							option.style.display = "none";
							getById("multiselect1").checked = false;
							try {
								getById("multiselect1").parentNode.style.display = "none";
							} catch (e) {}
							adMatch[j] = null;
							break;
						} else if (normalizeDeviceLabel(deviceInfo.label).includes(adMatch[j])) {
							option.checked = true;
							listele.style.display = "block";
							option.style.display = "none";
							getById("multiselect1").checked = false;
							try {
								getById("multiselect1").parentNode.style.display = "none";
							} catch (e) {}
							adMatch[j] = null;
							break;
						}
					}
				}

				if (typeof adMatch !== "object" && counter == 2) {
					option.checked = true;
					listele.style.display = "block";
					option.style.display = "none";
					getById("multiselect1").checked = false;
					try {
						getById("multiselect1").parentNode.style.display = "none";
					} catch (e) {}
				}

				option.value = deviceInfo.deviceId || "default";
				option.name = "multiselect" + counter;
				option.id = "multiselect" + counter;
				option.label = deviceInfo.label;

				label = document.createElement("label");
				label.for = option.name;

				label.innerHTML = " " + (deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1));

				listele.appendChild(option);
				listele.appendChild(label);
				audioInputSelect.appendChild(listele);

				option.onchange = function (event) {
					// make sure to clear 'no audio option' if anything else is selected
					getById("multiselect1").checked = false;
					log("UNCHECKED");
					if (!CtrlPressed) {
						SelectedAudioInputDevices = [];
						audioInputSelect.querySelectorAll("input[type='checkbox']").forEach(function (item) {
							if (event.currentTarget.id !== item.id) {
								item.checked = false;
							} else {
								item.checked = true;
								SelectedAudioInputDevices = [event.currentTarget.value];
							}
						});
					} else {
						if (event.currentTarget.checked) {
							if (!SelectedAudioInputDevices) {
								SelectedAudioInputDevices = [event.currentTarget.value];
							} else if (!SelectedAudioInputDevices.includes(event.currentTarget.value)) {
								SelectedAudioInputDevices.push(event.currentTarget.value);
							}
						} else if (event.currentTarget.value) {
							while (SelectedAudioInputDevices.includes(event.currentTarget.value)) {
								SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(event.currentTarget.value), 1);
							}
						}
					}
					if (session.mobile && !(iOS || iPad) && event.currentTarget.label === "USB audio" && !session.cleanOutput) {
						warnUser("Notice: USB audio devices may not work on all mobile devices.\n\nConsider using FireFox mobile instead, as it tends to work with USB audio devices more often.");
					}
					saveSettings();
				};

				if (deviceInfo.label.includes("Yeti ")) {
					if (!session.cleanOutput) {
						//getById("audioTipContext1").innerHTML = getTranslation("blue-yeti-tip");
						miniTranslate(getById("audioTipContext1"), "blue-yeti-tip");
						getById("audioTip1").classList.remove("hidden");
					}
				}
			} else if (deviceInfo.kind === "videoinput") {
				option = document.createElement("option");
				option.value = deviceInfo.deviceId || "default";
				option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
				videoSelect.appendChild(option);
			} else if (deviceInfo.kind === "audiooutput") {
				option = document.createElement("option");
				if (audioOutputSelect.length === 0) {
					option.dataset.default = true;
				} else {
					option.dataset.default = false;
				}
				option.value = deviceInfo.deviceId || "default";
				if (option.value == session.sink) {
					option.selected = "true";
				} else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices == option.value) {
					option.selected = "true";
				}
				option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`;
				audioOutputSelect.appendChild(option);
			} else {
				log("Some other kind of source/device: ", deviceInfo);
			}
		}

		if (Firefox && !session.mobile) {
			var option = document.createElement("option");
			option.value = "others";
			option.text = getTranslation("show-more-options");
			audioOutputSelect.appendChild(option);
		}

		if (audioOutputSelect.childNodes.length == 0) {
			option = document.createElement("option");
			option.value = "default";
			option.text = getTranslation("system-default");
			audioOutputSelect.appendChild(option);
		}

		option = document.createElement("option");
		option.text = getTranslation("disable-video");
		option.value = "ZZZ";
		videoSelect.appendChild(option); // NO AUDIO OPTION

		if (miconly) {
			option.selected = "true";
		}

		selectors.forEach((select, selectorIndex) => {
			if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
				select.value = values[selectorIndex];
			}
		});
	} catch (e) {
		errorlog(e);
	}
}


function getUserMediaVideoParams(resolutionFallbackLevel, isSafariBrowser) {
	switch (resolutionFallbackLevel) {
		case -1:
			return {};
		case -2:
			if (isSafariBrowser) {
				return {
					width: {
						min: 360,
						ideal: 3840,
						max: 3840
					},
					height: {
						min: 360,
						ideal: 2160,
						max: 2160
					}
				};
			} else if (Firefox) {
				return {
					width: {
						ideal: 3840
					},
					height: {
						ideal: 2160
					}
				};
			} else {
				return {
					width: {
						min: 720,
						ideal: 3840,
						max: 3840
					},
					height: {
						min: 720,
						ideal: 2160,
						max: 2160
					}
				};
			}
		case -3:
			if (isSafariBrowser) {
				return {
					width: {
						min: 360,
						ideal: 2560,
						max: 1440
					},
					height: {
						min: 360,
						ideal: 1440,
						max: 1440
					}
				};
			} else if (Firefox) {
				return {
					width: {
						ideal: 2560
					},
					height: {
						ideal: 1440
					}
				};
			} else {
				return {
					width: {
						min: 720,
						ideal: 2560,
						max: 2560
					},
					height: {
						min: 720,
						ideal: 1440,
						max: 1440
					}
				};
			}
		case 0:
			if (isSafariBrowser) {
				return {
					width: {
						min: 360,
						ideal: 1920,
						max: 1920
					},
					height: {
						min: 360,
						ideal: 1080,
						max: 1080
					}
				};
			} else if (Firefox) {
				return {
					width: {
						ideal: 1920
					},
					height: {
						ideal: 1080
					}
				};
			} else {
				return {
					width: {
						min: 720,
						ideal: 1920,
						max: 1920
					},
					height: {
						min: 720,
						ideal: 1080,
						max: 1920
					}
				};
			}
		case 1:
			if (isSafariBrowser) {
				return {
					width: {
						min: 360,
						ideal: 1280,
						max: 1280
					},
					height: {
						min: 360,
						ideal: 720,
						max: 720
					}
				};
			} else if (Firefox) {
				return {
					width: {
						ideal: 1280
					},
					height: {
						ideal: 720
					}
				};
			} else {
				return {
					width: {
						min: 720,
						ideal: 1280,
						max: 1280
					},
					height: {
						min: 720,
						ideal: 720,
						max: 1280
					}
				};
			}
		case 2:
			if (isSafariBrowser) {
				return {
					width: {
						min: 640
					},
					height: {
						min: 360
					}
				};
			} else if (Firefox) {
				return {
					width: {
						ideal: 640
					},
					height: {
						ideal: 360
					}
				};
			} else {
				return {
					width: {
						min: 240,
						ideal: 640,
						max: 1280
					},
					height: {
						min: 240,
						ideal: 360,
						max: 1280
					}
				};
			}
		case 3:
			if (isSafariBrowser) {
				return {
					width: {
						min: 360,
						ideal: 1280,
						max: 1440
					}
				};
			} else {
				return {
					width: {
						min: 360,
						ideal: 1280,
						max: 1440
					}
				};
			}
		case 4:
			if (isSafariBrowser) {
				return {
					height: {
						min: 360,
						ideal: 720,
						max: 960
					}
				};
			} else {
				return {
					height: {
						ideal: 720,
						max: 960
					}
				};
			}
		case 5:
			if (isSafariBrowser) {
				return {
					width: {
						min: 360,
						ideal: 640,
						max: 1440
					},
					height: {
						min: 360,
						ideal: 360,
						max: 720
					}
				};
			} else {
				return {
					width: {
						ideal: 640,
						max: 1920
					},
					height: {
						ideal: 360,
						max: 1920
					}
				}; // same as default, but I didn't want to mess with frameRates until I gave it all a try first
			}
		case 6:
			if (isSafariBrowser) {
				return {}; // iphone users probably don't need to wait any longer, so let them just get to it
			} else {
				return {
					width: {
						min: 360,
						ideal: 640,
						max: 3840
					},
					height: {
						min: 360,
						ideal: 360,
						max: 2160
					}
				};
			}
		case 7:
			return {
				// If the camera is recording in low-light, it may have a low frameRate. It coudl also be recording at a very high resolution.
				width: {
					min: 360,
					ideal: 640
				},
				height: {
					min: 360,
					ideal: 360
				}
			};

		case 8:
			return {
				width: {
					min: 360
				},
				height: {
					min: 360
				},
				frameRate: 10
			}; // same as default, but I didn't want to mess with frameRates until I gave it all a try first
		case 9:
			return {
				frameRate: 0
			}; // Some Samsung Devices report they can only support a frameRate of 0.
		case 10:
			return {};
		default:
			return {};
	}
}

function addScreenDevices(device) {
	if (device.kind == "audio") {
		const audioInputSelect = getById("audioSource3");
		const listele = document.createElement("li");
		listele.style.display = "block";

		const option = document.createElement("input");
		option.type = "checkbox";
		option.checked = true;

		if (getById("multiselect-trigger3").dataset.state == 0) {
			option.style.display = "none";
		}

		option.value = device.id;
		option.name = device.label;
		option.dataset.type = "screen";
		option.label = device.label;

		const label = document.createElement("label");
		label.for = option.name;
		label.innerHTML = " " + device.label;
		listele.appendChild(option);
		listele.appendChild(label);

		option.onchange = function (event) {
			// make sure to clear 'no audio option' if anything else is selected
			log("change 4644");
			if (!CtrlPressed) {
				document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
					if (!item.value) {
						return;
					}
					if (event.currentTarget.value !== item.value) {
						// this shoulnd't happen, but if it does.

						item.checked = false;

						if (item.dataset.type == "screen") {
							item.parentElement.parentElement.removeChild(item.parentElement);
						}

						while (SelectedAudioInputDevices.indexOf(item.value) > -1) {
							SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1);
						}

						activatedPreview = false;
						grabAudio("#audioSource3"); // exclude item.id
					} else {
						if (SelectedAudioInputDevices.indexOf(item.value) == -1) {
							if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
								SelectedAudioInputDevices = [];
							}
							SelectedAudioInputDevices.push(item.value);
						}

						item.checked = true;
						activatedPreview = false;
						grabAudio("#audioSource3", item.value); // exclude item.id.   we will reconnect, even if already connected, as a way to 'reset' a device if it isn't working.
					}
				});
			}
			saveSettings();
			event.stopPropagation();
			return false;
		};
		audioInputSelect.appendChild(listele);
		getById("audioSourceNoAudio2").checked = false;
	} else if (device.kind == "video") {
		const videoSelect = getById("videoSource3");
		//const selectors = [ videoSelect];
		//const values = selectors.map(select => select.value);
		const option = document.createElement("option");
		option.value = device.id;
		option.text = device.label;
		option.selected = "true";
		option.label = device.label;
		videoSelect.appendChild(option);
	}
}

var gotDevices2AlreadyRan = false;
function gotDevices2(deviceInfos) {
	gotDevices2AlreadyRan = true;
	log("got devices!2");
	log(deviceInfos);
	getById("multiselect-trigger3").dataset.state = "0";
	getById("multiselect-trigger3").classList.add("closed");
	getById("multiselect-trigger3").classList.remove("open");
	getById("chevarrow2").classList.add("bottom");

	if (!session.streamSrc) {
		checkBasicStreamsExist();
	}

	var knownTrack = false;

	try {
		const audioInputSelect = getById("audioSource3");
		const videoSelect = getById("videoSource3");
		const audioOutputSelect = getById("outputSource3");
		const selectors = [videoSelect];

		[audioInputSelect].forEach(select => {
			while (select.firstChild) {
				select.removeChild(select.firstChild);
			}
		});

		const values = selectors.map(select => select.value);
		selectors.forEach(select => {
			while (select.firstChild) {
				select.removeChild(select.firstChild);
			}
		});

		[audioOutputSelect].forEach(select => {
			while (select.firstChild) {
				select.removeChild(select.firstChild);
			}
		});

		var counter = 0;
		for (let i = 0; i !== deviceInfos.length; ++i) {
			const deviceInfo = deviceInfos[i];
			if (deviceInfo == null) {
				continue;
			}

			if (deviceInfo.kind === "audioinput") {
				var option = document.createElement("input");
				option.type = "checkbox";
				counter++;
				var listele = document.createElement("li");
				listele.style.display = "none";

				session.streamSrc.getAudioTracks().forEach(function (track) {
					if (deviceInfo.label == track.label) {
						option.checked = true;
						listele.style.display = "inherit";
					}
				});

				option.style.display = "none";
				option.value = deviceInfo.deviceId || "default";
				option.name = "multiselecta" + counter;
				option.id = "multiselecta" + counter;
				option.dataset.label = deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1);

				var label = document.createElement("label");
				label.for = option.name;

				label.innerHTML = " " + (deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1));

				listele.appendChild(option);
				listele.appendChild(label);
				audioInputSelect.appendChild(listele);

				option.onchange = function (event) {
					// make sure to clear 'no audio option' if anything else is selected
					log("change 4768");
					if (!CtrlPressed) {
						document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
							if (event.currentTarget.value !== item.value) {
								item.checked = false;
								if (item.dataset.type == "screen") {
									item.parentElement.parentElement.removeChild(item.parentElement);
								}
								while (SelectedAudioInputDevices.indexOf(item.value) > -1) {
									SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1);
								}
							} else {
								item.checked = true;
								if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) {
									if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
										SelectedAudioInputDevices = [];
									}
									SelectedAudioInputDevices.push(event.currentTarget.value);
								}
							}
						});
					} else {
						if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) {
							if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
								SelectedAudioInputDevices = [];
							}
							SelectedAudioInputDevices.push(event.currentTarget.value);
						}

						getById("audioSourceNoAudio2").checked = false;
					}
					saveSettings();
				};
			} else if (deviceInfo.kind === "videoinput") {
				var option = document.createElement("option");
				option.value = deviceInfo.deviceId || "default";
				option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
				try {
					if (!knownTrack && session.canvasSource) {
						session.canvasSource.srcObject.getVideoTracks().forEach(function (track) {
							if (option.text == track.label) {
								option.selected = "true";
								knownTrack = true;
							}
						});
					}
					if (!knownTrack && session.streamSrc) {
						session.streamSrc.getVideoTracks().forEach(function (track) {
							if (option.text == track.label) {
								option.selected = "true";
								knownTrack = true;
							}
						});
					}
				} catch (e) {
					errorlog(e);
				}
				videoSelect.appendChild(option);
			} else if (deviceInfo.kind === "audiooutput") {
				var option = document.createElement("option");
				if (audioOutputSelect.length === 0) {
					option.dataset.default = true;
				} else {
					option.dataset.default = false;
				}
				option.value = deviceInfo.deviceId || "default";
				if (option.value == session.sink) {
					option.selected = "true";
				} else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices == option.value) {
					option.selected = "true";
					session.sink = option.value; // added 8-dec-22, as the director's saved mic wasn't applying otherwise.
				}
				option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`;
				audioOutputSelect.appendChild(option);
			} else {
				log("Some other kind of source/device: ", deviceInfo);
			}
		}

		if (Firefox && !session.mobile) {
			var option = document.createElement("option");
			option.value = "others";
			option.text = getTranslation("show-more-options");
			audioOutputSelect.appendChild(option);
		}

		if (audioOutputSelect.childNodes.length == 0) {
			var option = document.createElement("option");
			option.value = "default";
			option.text = getTranslation("system-default");
			audioOutputSelect.appendChild(option);
		}

		if (videoSelect.childNodes.length <= 1) {
			getById("flipcamerabutton").style.display = "none"; // don't show the camera cycle button
			getById("flipcamerabutton").dataset.maxndex = videoSelect.childNodes.length;
		} else {
			getById("flipcamerabutton").style.display = "unset";
			getById("flipcamerabutton").dataset.maxIndex = videoSelect.childNodes.length;
		}

		////////////
		session.streamSrc.getAudioTracks().forEach(function (track) {
			// add active ScreenShare audio tracks to the list
			log("Checking for screenshare audio");
			var matched = false;
			for (var i = 0; i !== deviceInfos.length; ++i) {
				var deviceInfo = deviceInfos[i];
				if (deviceInfo == null) {
					continue;
				}
				log("---");
				if (track.label == deviceInfo.label) {
					matched = true;
					continue;
				}
			}
			if (matched == false) {
				// Not a gUM device
				var listele = document.createElement("li");
				listele.style.display = "block";
				var option = document.createElement("input");
				option.type = "checkbox";
				option.value = track.id;
				option.checked = true;
				option.style.display = "none";
				option.name = track.label;
				option.label = track.label;
				option.dataset.type = "screen";
				var label = document.createElement("label");
				label.for = option.name;
				label.innerHTML = " " + track.label;
				listele.appendChild(option);
				listele.appendChild(label);
				option.onchange = function (event) {
					// make sure to clear 'no audio option' if anything else is selected
					log("change 4873");
					var trackid = null;
					if (!CtrlPressed) {
						document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
							if (event.currentTarget.value !== item.value) {
								// this shoulnd't happen, but if it does.
								item.checked = false;
								if (item.dataset.type == "screen") {
									item.parentElement.parentElement.removeChild(item.parentElement);
								}
							} else {
								event.currentTarget.checked = true;
								trackid = item.value;
							}
						});
					} else {
						//getById("audioSourceNoAudio2").checked=false;
						if (event.currentTarget.dataset.type == "screen") {
							event.currentTarget.parentElement.parentElement.removeChild(event.currentTarget.parentElement);
						}
					}
					activatedPreview = false;
					grabAudio("#audioSource3", trackid); // exclude item.id.
					event.stopPropagation();
					return false;
				};
				audioInputSelect.appendChild(listele);
			}
		});

		/////////// no video option
		var optionss = false;
		if (screensharesupport) {
			optionss = document.createElement("option");
			optionss.text = "Screen Share (replace camera)";
			optionss.value = "XXX";
			videoSelect.appendChild(optionss); // NO AUDIO OPTION
		}

		var option = document.createElement("option"); // no video
		option.text = getTranslation("disable-video");
		option.value = "ZZZ";
		videoSelect.appendChild(option);

		if (session.streamSrc.getVideoTracks().length == 0) {
			option.selected = "true";
		} else if (knownTrack == false) {
			var option = document.createElement("option"); // no video
			option.text = session.streamSrc.getVideoTracks()[0].label;
			option.value = "YYY";
			videoSelect.appendChild(option);
			option.selected = "true";
		}

		if (optionss) {
			optionss.lastSelected = videoSelect.selectedIndex;
		}

		videoSelect.onchange = function (event) {
			try {
				if (event.target.options[event.target.options.selectedIndex].value === "XXX") {
					videoSelect.selectedIndex = event.target.options[event.target.options.selectedIndex].lastSelected;
					if (session.screenShareState == false) {
						toggleScreenShare();
					} else {
						toggleScreenShare(true);
					}
					return;
				}
			} catch (e) {}
			activatedPreview = false;
			grabVideo(session.quality, "videosource", "select#videoSource3");

			if (!getById("audioSource3").querySelectorAll("input[data-type='screen']").length) {
				if (session.screenShareState) {
					session.screenShareState = false;
					pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
					notifyOfScreenShare();
					//session.refreshScale();
				}
				getById("screensharebutton").classList.remove("green");
				getById("screensharebutton").ariaPressed = "false";
			}
		};

		/////////////  /// NO AUDIO appended option

		var option = document.createElement("input");
		option.type = "checkbox";
		option.value = "ZZZ";
		option.style.display = "none";
		option.id = "audioSourceNoAudio2";

		var label = document.createElement("label");
		label.for = option.name;
		label.innerHTML = " No Audio";
		var listele = document.createElement("li");

		if (session.streamSrc.getAudioTracks().length == 0) {
			option.checked = true;
		} else {
			listele.style.display = "none";
			option.checked = false;
		}
		option.onchange = function (event) {
			// make sure to clear 'no audio option' if anything else is selected
			log("change 4938");
			if (!CtrlPressed) {
				document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
					if (event.currentTarget.value !== item.value) {
						item.checked = false;
						if (item.dataset.type == "screen") {
							item.parentElement.parentElement.removeChild(item.parentElement);
						}

						while (SelectedAudioInputDevices.indexOf(item.value) > -1) {
							SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1);
						}
					} else {
						item.checked = true;
						if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) {
							if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
								SelectedAudioInputDevices = [];
							}
							SelectedAudioInputDevices.push(event.currentTarget.value);
						}
					}
				});
			} else {
				document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
					if (event.currentTarget.value === item.value) {
						event.currentTarget.checked = true;
						if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) {
							if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
								SelectedAudioInputDevices = [];
							}
							SelectedAudioInputDevices.push(event.currentTarget.value);
						}
					} else {
						item.checked = false;
						if (item.dataset.type == "screen") {
							item.parentElement.parentElement.removeChild(item.parentElement);
						}
						while (SelectedAudioInputDevices.indexOf(item.value) > -1) {
							SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1);
						}
					}
				});
			}
			saveSettings();
		};
		listele.appendChild(option);
		listele.appendChild(label);
		audioInputSelect.appendChild(listele);

		////////////

		//selectors.forEach((select, selectorIndex) => {
		//	if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
		//		select.value = values[selectorIndex];
		//	}
		//});

		audioInputSelect.onchange = function () {
			log("Audio OPTION HAS CHANGED? 2");
			activatedPreview = false;
			setTimeout(function () {
				grabAudio("#audioSource3");
			}, 10);
		};

		getById("refreshVideoButton").onclick = function () {
			refreshVideoDevice();
		};

		if (Firefox && !session.mobile && navigator.mediaDevices) {
			audioOutputSelect.onclick = function () {
				log("audioOutputSelect.onclick = function() {");
				if (audioOutputSelect.options[audioOutputSelect.selectedIndex].value === "others") {
					log("Trying to increase the output device list");
					navigator.mediaDevices.selectAudioOutput().then(device => {
						if (device.kind == "audiooutput") {
							session.sink = device.deviceId;

							try {
								var matched = false;
								audioOutputSelect.childNodes.forEach(ele => {
									if (ele.value === device.deviceId) {
										matched = true;
										ele.selected = true;
									}
								});
								if (!matched) {
									var option = document.createElement("option");
									option.value = device.deviceId;
									option.text = device.label;
									audioOutputSelect.appendChild(option);
									option.selected = true;
								}

								saveSettings(); // we're saving because there was an explicit action to change devices
							} catch (e) {
								errorlog(e);
							}

							if (!session.sink) {
								return;
							} // Not sure this would ever happen, but whatever.

							resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist.
						}
					});
				}
			};
		} else if (!navigator.mediaDevices){
			console.warn("No navigator.mediaDevices found - try a different browser or check your settings.");
		}

		audioOutputSelect.onchange = function () {
			log("audioOutputSelect.onchange = function() {");

			if (iOS || iPad) {
				return;
			}

			if (Firefox && !session.mobile) {
				if (audioOutputSelect.options[audioOutputSelect.selectedIndex].value === "others") {
					// we handle this elsewhere
					return;
				}
			}

			try {
				session.sink = audioOutputSelect.options[audioOutputSelect.selectedIndex].value;
				saveSettings();
			} catch (e) {
				errorlog(e);
			}
			if (!session.sink) {
				return;
			}

			resetupAudioOut();

			log("done audioOutputSelect.onchange = function() {");
		};
	} catch (e) {
		errorlog(e);
	}
}

function refreshVideoDevice() {
	if (session.screenShareState) {
		log("can't refresh a screenshare");
		return;
	}
	log("video source changed");
	activatedPreview = false;
	grabVideo(session.quality, "videosource", "select#videoSource3");
}

function refreshMicrophoneDevice(UUID=false) {
	if (session.screenShareState || session.mediafileShare) {
		log("can't refresh a screenshare or fileshare");
		
		if (UUID){
			var data = {};
			data.UUID = UUID;
			data.rejected = "can't refresh mic during screen or file share";
			session.sendMessage(data, data.UUID);
		}
		
		return;
	}
	log("refreshing microphone..");
	activatedPreview = false;
	grabAudio("#audioSource3", null, false, UUID);	
}

function directRefreshMicrophone(ele){
	var UUID = ele.dataset.UUID;
	if (!UUID){return;}
	//  remoteAudioLabel_
	// '[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]'
	
	if (getById("remoteAudioLabel_" + UUID).dataset.nomic){
		warnUser("No microphone is enabled for this user\n\nNothing to reresh.");
	}
	var data = {};
	data.refreshMicrophone = true;
	data.UUID = UUID;
	if (session.sendRequest(data, UUID)){ // Viewer is requesting the PUBLISHER
		ele.classList.add("pressed");
		setTimeout((ele)=>{
			if(ele){
				ele.classList.remove("pressed");
			}
		},400,ele);
	}
}

function gotDevicesRemote(deviceInfos, UUID) {
	try {
		if (document.getElementById("remoteVideoSelect_" + UUID)) {
			var videoSelect = document.getElementById("remoteVideoSelect_" + UUID);
			var length = videoSelect.options.length;
			for (i = length - 1; i >= 0; i--) {
				videoSelect.options[i] = null;
			}
		} else {
			var videoSelect = document.createElement("select");
			videoSelect.id = "remoteVideoSelect_" + UUID;

			videoSelect.onchange = function () {
				if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) {
					getById("requestVideoDevice_" + UUID).innerHTML = '<i class="las la-video"></i> apply';
					getById("requestVideoDevice_" + UUID).title = "This will update the remote device to the selected one";
				} else {
					getById("requestVideoDevice_" + UUID).innerHTML = '<i class="las la-video"></i> request';
					getById("requestVideoDevice_" + UUID).title = "This will ask the remote guest for permission to change";
				}
			};

			var buttonGO = document.createElement("button");
			buttonGO.innerHTML = '<i class="las la-video"></i> refresh';
			buttonGO.title = "This will refresh the current device";
			buttonGO.id = "requestVideoDevice_" + UUID;
			buttonGO.onclick = function () {
				var data = {};
				data.changeCamera = videoSelect.value;
				data.UUID = UUID;
				session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER
			};

			var videoSelectDiv = document.createElement("div");
			query("#container_" + UUID + " .advancedVideoSettings").appendChild(videoSelectDiv);
			videoSelectDiv.appendChild(videoSelect);
			videoSelectDiv.appendChild(buttonGO);
		}

		if (document.getElementById("remoteAudioSelect_" + UUID)) {
			log("remoteAudioSelect_ ");
			var audioSelect = document.getElementById("remoteAudioSelect_" + UUID);
			var length = audioSelect.options.length;
			for (i = length - 1; i >= 0; i--) {
				audioSelect.options[i] = null;
			}
		} else {
			var audioSelect = document.createElement("select");
			audioSelect.id = "remoteAudioSelect_" + UUID;

			audioSelect.onchange = function () {
				log("ON CHANGE");
				if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) {
					getById("requestAudioDevice_" + UUID).innerHTML = '<i class="las la-microphone-alt"></i> apply';
					getById("requestAudioDevice_" + UUID).title = "This will update the remote device to the selected one";
				} else {
					getById("requestAudioDevice_" + UUID).innerHTML = '<i class="las la-microphone-alt"></i> request';
					getById("requestAudioDevice_" + UUID).title = "This will ask the remote guest for permission to change";
				}
			};

			var buttonGO = document.createElement("button");
			buttonGO.innerHTML = '<i class="las la-microphone-alt"></i> refresh';
			// buttonGO.style = "padding: 5px;";
			buttonGO.title = "This will refresh the current device";
			buttonGO.id = "requestAudioDevice_" + UUID;

			buttonGO.onclick = function () {
				var data = {};
				data.changeMicrophone = audioSelect.value;
				data.UUID = UUID;
				session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER
			};
			var audioSelectDiv = document.createElement("div");
			query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioSelectDiv);
			audioSelectDiv.appendChild(audioSelect);
			audioSelectDiv.appendChild(buttonGO);
		}

		if (document.getElementById("remoteAudioOutputSelect_" + UUID)) {
			var audioOutputSelect = document.getElementById("remoteAudioOutputSelect_" + UUID);
			var length = audioOutputSelect.options.length;
			for (i = length - 1; i >= 0; i--) {
				audioOutputSelect.options[i] = null;
			}
		} else {
			var audioOutputSelect = document.createElement("select");
			audioOutputSelect.id = "remoteAudioOutputSelect_" + UUID;

			audioOutputSelect.onchange = function () {
				if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) {
					getById("requestAudioOutputDevice_" + UUID).innerHTML = '<i class="las la-headphones"></i> apply';
					getById("requestAudioOutputDevice_" + UUID).title = "This will update the remote device to the selected one";
				} else {
					getById("requestAudioOutputDevice_" + UUID).innerHTML = '<i class="las la-headphones"></i> request';
					getById("requestAudioOutputDevice_" + UUID).title = "This will ask the remote guest for permission to change";
				}
			};

			var buttonGO = document.createElement("button");
			buttonGO.innerHTML = '<i class="las la-headphones"></i> refresh';
			buttonGO.title = "This will refresh the current device";
			buttonGO.id = "requestAudioOutputDevice_" + UUID;
			buttonGO.onclick = function () {
				var data = {};
				data.changeSpeaker = audioOutputSelect.value;
				data.UUID = UUID;
				session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER
			};

			var audioOutputSelectContainer = document.createElement("div");
			query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioOutputSelectContainer);
			audioOutputSelectContainer.appendChild(audioOutputSelect);
			audioOutputSelectContainer.appendChild(buttonGO);
		}

		var matched = false;
		var audiomatched = false;
		for (let i = 0; i !== deviceInfos.length; ++i) {
			const deviceInfo = deviceInfos[i];
			if (deviceInfo == null) {
				continue;
			}
			if (deviceInfo.kind === "videoinput") {
				const option = document.createElement("option");
				option.value = deviceInfo.deviceId || "default";
				option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
				if (getById("remoteVideoLabel_" + UUID).innerText == option.text) {
					option.selected = "true";
					matched = true;
				}
				videoSelect.appendChild(option);
			} else if (deviceInfo.kind === "audioinput") {
				const option = document.createElement("option");
				option.value = deviceInfo.deviceId || "default";
				option.text = deviceInfo.label || `microphone ${audioSelect.length + 1}`;
				if (getById("remoteAudioLabel_" + UUID).innerText == option.text) {
					option.selected = "true";
					audiomatched = true;
				}
				audioSelect.appendChild(option);
			} else if (deviceInfo.kind === "audiooutput") {
				const option = document.createElement("option");
				option.value = deviceInfo.deviceId || "default";
				option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;
				if (getById("remoteAudioOutputSelect_" + UUID).innerText == option.text) {
					option.selected = "true";
				}
				audioOutputSelect.appendChild(option);
			}
		}

		if (!matched) {
			getById("requestVideoDevice_" + UUID).innerHTML = '<i class="las la-video"></i> request';
			getById("requestVideoDevice_" + UUID).title = "This will ask the remote guest for permission to change";
		}

		if (!audiomatched) {
			getById("requestAudioDevice_" + UUID).innerHTML = '<i class="las la-microphone-alt"></i> request';
			getById("requestAudioDevice_" + UUID).title = "This will ask the remote guest for permission to change";
		}
	} catch (e) {
		errorlog(e);
	}

	pokeIframeAPI("remote-devices-info", deviceInfos, UUID);
}


var timeoutTone = false;
function playtone(screen = false, tonename = "testtone") {
    if (timeoutTone) return;
	
	if (session.beepToNotify && tonename){
		if (session.beepToNotify!==true){
			let val = tonename.split("tone")[0];
			if (!session.beepToNotify.includes(val)){
				return;
			}
		}
	}
    
    const type = tonename.replace("tone", "");
    if (session.notifyTypes && !session.notifyTypes.includes(type)) return;

    timeoutTone = true;
    setTimeout(() => timeoutTone = false, 500);

    const toneEle = document.getElementById(tonename);
    if (!toneEle) return;

    if (iOS || iPad) {
        toneEle.muted = false;
        toneEle.play().catch(e => console.error('Playback failed:', e));
        return;
    }

    if (screen && getById("outputSourceScreenshare")) {
        session.sink = getById("outputSourceScreenshare").value;
        saveSettings();
    }

    if (session.sink) {
        toneEle.setSinkId(session.sink)
            .then(() => toneEle.play())
            .catch(() => toneEle.play());
    } else {
        toneEle.play();
    }
}

function updateAudioSource(newUrl, name="testtone") {
	var audioElement = document.getElementById(name);
	
	if (!audioElement){
		audioElement = createAudioElement();
		audioElement.id = name;
		getById("testtone").parentNode.insertBefore(audioElement, getById("testtone").nextSibling);
	} 
	
	audioElement.src = newUrl;
	
	var sources = audioElement.getElementsByTagName("source");
	var extension = newUrl.split(".").pop().toLowerCase();
	var mimeType;

	switch (extension) {
		case "mp3":
			mimeType = "audio/mpeg";
			break;
		case "wav":
			mimeType = "audio/wav";
			break;
		case "ogg":
			mimeType = "audio/ogg";
			break;
		case "aac":
		case "m4a":
			mimeType = "audio/aac";
			break;
		case "opus":
			mimeType = "audio/opus";
			break;
		case "flac":
			mimeType = "audio/flac";
			break;
		case "webm":
			mimeType = "audio/webm";
			break;
		default:
			console.error("Unsupported file type:", extension);
			return;
	}
	if (sources && sources.length === 1) {
		sources[0].src = newUrl;
		sources[0].type = mimeType;
	} else {
		audioElement.innerHTML = "";
		var newSource = document.createElement("source");
		newSource.src = newUrl;
		newSource.type = mimeType;
		audioElement.appendChild(newSource);
	}
	audioElement.load();
}

function showNotification(title, body = "") {
	if (!Notification) {
		return;
	}
	if (Notification.permission !== "granted") {
		Notification.requestPermission();
	} else {
		let icon = "/media/old_icon.png"; //this is a large image may take more time to show notifiction, replace with small size icon

		let notification = new Notification(title, { body, icon });

		notification.onclick = () => {
			notification.close();
			window.parent.focus();
		};
	}
}

function toFirefoxConstraint(chromeConstraint) {
    if (!chromeConstraint || typeof chromeConstraint !== 'object') {
        return chromeConstraint;
    }

    const result = { ...chromeConstraint };
	
	if (result.audio && typeof result.audio === 'object') {
        result.audio = flattenConstraints(result.audio);
    }

    if (result.video && typeof result.video === 'object') {
        result.video = flattenConstraints(result.video);
    }

	return result;
}

function flattenConstraints(constraints) {
    const result = { ...constraints };
    
    for (const [key, value] of Object.entries(constraints)) {
        if (value && typeof value === 'object') {
            if ('exact' in value) {
                result[key] = value.exact;
            } else if ('ideal' in value) {
                result[key] = value.ideal;
            }
        }
    }

    return result;
}

async function getAudioOnly(selector, trackid = null, override = false) {
	var audioSelect = document.querySelector(selector).querySelectorAll("input,option");
	var audioList = [];
	var streams = [];
	log("getAudioOnly()");
	for (var i = 0; i < audioSelect.length; i++) {
		if (audioSelect[i].value == "ZZZ") {
			continue;
		} else if (trackid == audioSelect[i].value) {
			// skip already excluded
			continue;
		} else if ("screen" == audioSelect[i].dataset.type) {
			// skip already excluded ---------- !!!!!!  DOES THIS MAKE SENSE? TODO: CHECK
			continue;
		} else if (audioSelect[i].checked) {
			log(audioSelect[i]);
			audioList.push(audioSelect[i]);
		} else if (audioSelect[i].selected) {
			log(audioSelect[i]);
			audioList.push(audioSelect[i]);
		}
	}

	for (var i = 0; i < audioList.length; i++) {
		if (session.echoCancellation !== false && session.autoGainControl !== false && session.noiseSuppression !== false && (session.voiceIsolation !== true)) {
			var constraint = {
				audio: {
					deviceId: {
						exact: audioList[i].value
					}
				}
			};
		} else {
			// Just trying to avoid problems with some systems that don't support these features
			var constraint = {
				audio: {
					deviceId: {
						exact: audioList[i].value
					}
				}
			};
			if (session.echoCancellation === false) {
				constraint.audio.echoCancellation = false;
			} else {
				constraint.audio.echoCancellation = true;
			}
			if (session.autoGainControl === false) {
				constraint.audio.autoGainControl = false;
			} else {
				constraint.audio.autoGainControl = true;
			}
			if (session.noiseSuppression === false) {
				constraint.audio.noiseSuppression = false;
			} else {
				constraint.audio.noiseSuppression = true;
			}
			if (session.voiceIsolation === true){
				constraint.audio.voiceIsolation = true;
			}
		}
		constraint.video = false;
		if (override !== false) {
			log("Override true");
			if (override.audio && override.audio.deviceId) {
				if (audioList[i].value == override.audio.deviceId) {
					constraint = override;
				} else {
					// not the device we want to hack.
				}
			} else {
				constraint = override;
			}
		}

		if (audioList[i].value && SelectedAudioInputDevices) {
			if (SelectedAudioInputDevices.indexOf(audioList[i].value) === -1) {
				if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
					SelectedAudioInputDevices = [];
				}
				SelectedAudioInputDevices.push(audioList[i].value);
			}
		}

		if (session.audioInputChannels) {
			if (constraint.audio === true) {
				constraint.audio = {};
				constraint.audio.channelCount = session.audioInputChannels;
			} else if (constraint.audio) {
				constraint.audio.channelCount = session.audioInputChannels;
			}
		}

		if (session.micSampleRate) {
			if (constraint.audio === true) {
				constraint.audio = {};
				constraint.audio.sampleRate = parseInt(session.micSampleRate);
			} else if (constraint.audio) {
				constraint.audio.sampleRate = parseInt(session.micSampleRate);
			}
		}

		if (session.micSampleSize) {
			if (constraint.audio === true) {
				constraint.audio = {};
				constraint.audio.sampleSize = parseInt(session.micSampleSize);
			} else if (constraint.audio) {
				constraint.audio.sampleSize = parseInt(session.micSampleSize);
			}
		}

		log("CONSTRAINT");
		log(constraint);
		
		if (Firefox){
			constraint = toFirefoxConstraint(constraint);
		}
		
		warnlog("navigator.mediaDevices.getUserMedia starting...");
		if (navigator.mediaDevices){
			var stream = await navigator.mediaDevices
				.getUserMedia(constraint)
				.then(function (stream2) {
					log("get audio sucecss");
					pokeIframeAPI("local-microphone-event");
					return stream2;
				})
				.catch(function (err) {
					warnlog(err);
					if (!session.cleanOutput) {
						if (override !== false) {
							if (err.name) {
								if (err.constraint) {
									warnUser(err["name"] + ": " + err["constraint"]);
								}
							}
						}
					}
					return false;
				}); // More error reporting maybe?
			if (stream) {
				streams.push(stream);
			}
		} else {
			console.warn("navigator.mediaDevices was not found; try a different browser or check your settings");
		}
	}

	return streams;
}

function applyMirror(mirror) {
	// true unmirrors as its already mirrored
	if (!session.videoElement) {
		return;
	}
	try {
		var transFlip = "";
		var transNorm = "";
		if (document.getElementById("videosource") && session.windowed) {
			transFlip = " translate(0, 50%)";
			transNorm = " translate(0, -50%)";
		}

		if (session.mirrored == 2) {
			mirror = true;
		} else if (session.mirrored === 0) {
			mirror = true;
		}

		if (!session.videoElement.style) {
			session.videoElement.style = "";
		}

		if (session.permaMirrored) {
			mirror = !mirror;
		}

		if (mirror) {
			if (session.mirrored && session.flipped) {
				session.videoElement.style.transform = "scaleX(-1) scaleY(-1)" + transFlip;
				session.videoElement.classList.add("mirrorControl");
				session.videoElement.dataset.transform = "scaleX(-1) scaleY(-1)";
			} else if (session.mirrored) {
				session.videoElement.style.transform = "scaleX(-1)" + transNorm;
				session.videoElement.classList.add("mirrorControl");
				session.videoElement.dataset.transform = "scaleX(-1)";
			} else if (session.flipped) {
				session.videoElement.style.transform = "scaleY(-1) scaleX(1)" + transFlip;
				session.videoElement.classList.remove("mirrorControl");
				session.videoElement.dataset.transform = "scaleX(1) scaleY(-11)";
			} else {
				session.videoElement.style.transform = "scaleX(1)" + transNorm;
				session.videoElement.classList.remove("mirrorControl");
				session.videoElement.dataset.transform = "scaleX(1)";
			}
		} else {
			if (session.mirrored && session.flipped) {
				session.videoElement.style.transform = "scaleX(1) scaleY(-1)" + transFlip;
				session.videoElement.classList.remove("mirrorControl");
				session.videoElement.dataset.transform = "scaleX(1) scaleY(-1)";
			} else if (session.mirrored) {
				session.videoElement.style.transform = "scaleX(1)" + transNorm;
				session.videoElement.classList.remove("mirrorControl");
				session.videoElement.dataset.transform = "scaleX(1)";
			} else if (session.flipped) {
				session.videoElement.style.transform = "scaleY(-1) scaleX(-1)" + transFlip;
				session.videoElement.classList.add("mirrorControl");
				session.videoElement.dataset.transform = "scaleX(-1) scaleY(-1)";
			} else {
				session.videoElement.style.transform = "scaleX(-1)" + transNorm;
				session.videoElement.classList.add("mirrorControl");
				session.videoElement.dataset.transform = "scaleX(-1)";
			}
		}
		var rotate = 0;
		if (session.forceRotate !== false) {
			if (session.rotate) {
				rotate = session.forceRotate * -1 + parseInt(session.rotate);
			} else {
				rotate = session.forceRotate * -1;
			}
			if (session.forceRotate) {
				rotate += 180;
			}
		} else {
			rotate = session.rotate;
		}

		if (rotate && rotate >= 360) {
			rotate -= 360;
		}

		session.videoElement.rotated = rotate;
		session.videoElement.dataset.rotated = rotate;

		if (document.getElementById("previewWebcam") || document.getElementById("videosource")) {
			var eleName = document.getElementById("previewWebcam") || document.getElementById("videosource");
			if (rotate) {
				if (eleName.style.transform) {
					eleName.style.transform += " rotate(" + rotate + "deg)";
				} else {
					eleName.style.transform = "rotate(" + rotate + "deg)";
				}
				eleName.classList.add("rotate");
			} else {
				eleName.classList.remove("rotate");
			}
		} else if (document.getElementById("container")) {
			if (rotate == 0) {
				document.getElementById("container").classList.remove("rotate");
				document.getElementById("container").style.transform = "unset";
				document.getElementById("container").style.transformOrigin = "unset";
			} else {
				document.getElementById("container").style.transform = "rotate(" + rotate + "deg)";
			}
		} else if (document.getElementById("minipreview")) {
			var eleName = document.getElementById("minipreview");
			if (rotate == 90) {
				eleName.style.transform = "rotate(90deg)";
				eleName.style.transformOrigin = "50% 100%";
				eleName.style.height = eleName.style.width;
				eleName.style.width = "unset";
			} else if (session.videoElement.rotated == 270) {
				eleName.style.transform = "rotate(270deg)";
				eleName.style.transformOrigin = "50% 100%";
				eleName.style.width = "unset";
				eleName.style.height = eleName.style.width;
			} else if (session.videoElement.rotated == 180) {
				eleName.style.transform = "rotate(180deg)";
				eleName.style.transformOrigin = "unset";
			} else {
				eleName.classList.remove("rotate");
				eleName.style.transform = "unset";
				eleName.style.transformOrigin = "unset";
			}
		}
		// if not one of these, then it's going to be handled by the automixer automatically for us.
	} catch (e) {
		errorlog(e);
	}
}

function applyMirrorGuest(mirror, videoElement) {
	// true unmirrors as its already mirrored
	try {
		if (mirror) {
			videoElement.style.transform = "scaleX(-1)";
			videoElement.classList.add("mirrorControl");
		} else {
			videoElement.style.transform = "scaleX(1)";
			videoElement.classList.remove("mirrorControl");
		}
	} catch (e) {
		errorlog(e);
	}
}

function cleanupMediaTracks() {
	getUserMediaRequestID += 1;
	try {
		if (session.streamSrcClone) {
			session.streamSrcClone.getTracks().forEach(function (track) {
				session.streamSrcClone.removeTrack(track);
				track.stop();
				log("stopping old track");
			});
		}
		if (session.streamSrc) {
			session.streamSrc.getTracks().forEach(function (track) {
				session.streamSrc.removeTrack(track);
				track.stop();
				log("stopping old track");
			});
		} else {
			checkBasicStreamsExist();
		}
		if (session.videoElement && session.videoElement.srcObject) {
			session.videoElement.srcObject.getTracks().forEach(function (track) {
				session.videoElement.srcObject.removeTrack(track);
				track.stop();
				log("stopping old track");
			});
		} else {
			session.videoElement.srcObject = createMediaStream();
		}
		activatedPreview = false;
	} catch (e) {
		errorlog(e);
	}
	getById("gowebcam").dataset.ready = "false";
	getById("gowebcam").dataset.audioready = "false";
	getById("gowebcam").disabled = true;
}

///  Detect system changes; handle change or use for debugging
var lastAudioDevice = null;
var lastVideoDevice = null;
var lastPlaybackDevice = null;

var audioReconnectTimeout = null;
var videoReconnectTimeout = null;
var grabDevicesTimeout = null;
var playbackReconnectTimeout = null;

function reconnectDevices(event) {
	///  TODO: Perhaps change this to only if there is a DISCONNECT; rather than ON NEW DEVICE?

	try {
		if (session.firstPlayTriggered && session.audioCtx.state == "suspended") {
			session.audioCtx.resume();
		}
	} catch (e) {
		warnlog("session.audioCtx.resume(); failed");
	}

	warnlog("A media device has changed");

	if (iOS || iPad) {
		// consider adding this back, but if no problem, whatever.
		return;
	}

	if (document.getElementById("previewWebcam")) {
		// rest of the code isn't setup to support pre-connection setup.
		clearTimeout(playbackReconnectTimeout);
		playbackReconnectTimeout = setTimeout(function () {
			if (document.getElementById("previewWebcam")) {
				enumerateDevices()
					.then(gotDevices)
					.then(function () {
						if (document.getElementById("previewWebcam")) {
							resetupAudioOut(document.getElementById("previewWebcam"));
						}
					});
			}
		}, 1000);
		return;
	}

	try {
		if (!session.streamSrc) {
			checkBasicStreamsExist();
		} else {
			session.streamSrc.getTracks().forEach(function (track) {
				if (track.readyState == "ended") {
					if (track.kind == "audio") {
						lastAudioDevice = track.label;
					} else if (track.kind == "video") {
						lastVideoDevice = track.label;
					}
					session.streamSrc.removeTrack(track);
					log("remove ended old track");
				}
			});

			if (session.streamSrcClone) {
				session.streamSrcClone.getTracks().forEach(function (track) {
					if (track.readyState == "ended") {
						session.streamSrcClone.removeTrack(track);
						track.stop();
					}
				});
			}

			if (session.videoElement && session.videoElement.srcObject) {
				session.videoElement.srcObject.getTracks().forEach(function (track) {
					if (track.readyState == "ended") {
						session.videoElement.srcObject.removeTrack(track);
						log("remove ended old track");
					}
				});
			}
		}
		/* } else {
			clearTimeout(playbackReconnectTimeout);
			playbackReconnectTimeout = setTimeout(function() {
				enumerateDevices().then(gotDevices2).then(function() {
					resetupAudioOut();
				});
			}, 1000);
			return;
		} */
	} catch (e) {
		errorlog(e);
	}

	clearTimeout(audioReconnectTimeout);
	audioReconnectTimeout = null;
	if (lastAudioDevice) {
		audioReconnectTimeout = setTimeout(function () {
			// only reconnect same audio device.  If reconnected, clear the disconnected flag.
			enumerateDevices()
				.then(gotDevices2)
				.then(function () {
					// TODO: check to see if any audio is connected?
					var streamConnected = false;
					var audioSelect = getById("audioSource3").querySelectorAll("input");
					for (var i = 0; i < audioSelect.length; i++) {
						if (audioSelect[i].value == "ZZZ") {
							continue;
						} else if (audioSelect[i].checked) {
							log("checked");
							streamConnected = true;
							break;
						}
					}

					if (!streamConnected) {
						for (var i = 0; i < audioSelect.length; i++) {
							if (audioSelect[i].value == "ZZZ") {
								continue;
							}
							if (lastAudioDevice == audioSelect[i].dataset.label) {
								// if the last disconnected device matches.
								audioSelect[i].checked = true;
								streamConnected = true;
								lastAudioDevice = null;
								warnlog("DISCONNECTED AUDIO DEVICE RECONNECTED");
								break;
							}
						}
					}
					activatedPreview = false;
					grabAudio("#audioSource3");
					setTimeout(function () {
						enumerateDevices()
							.then(gotDevices2)
							.then(function () {});
					}, 1000);
				});
		}, 2000);
	}

	clearTimeout(videoReconnectTimeout); // only reconnect same video device.
	videoReconnectTimeout = null;
	if (lastVideoDevice) {
		videoReconnectTimeout = setTimeout(function () {
			enumerateDevices()
				.then(gotDevices2)
				.then(function () {
					var streamConnected = false;
					var videoSelect = getById("videoSource3");
					errorlog(videoSelect.value);

					if (videoSelect.value == "ZZZ") {
						for (var i = 0; i < videoSelect.options.length; i++) {
							try {
								if (videoSelect.options[i].innerHTML == lastVideoDevice) {
									videoSelect.options[i].selected = "true";
									streamConnected = true;
									lastVideoDevice = null;
									break;
								}
							} catch (e) {
								errorlog(e);
							}
						}
					}

					if (streamConnected) {
						activatedPreview = false;
						grabVideo(session.quality, "videosource", "select#videoSource3");
						setTimeout(function () {
							enumerateDevices()
								.then(gotDevices2)
								.then(function () {});
						}, 1000);
					}
				});
		}, 2000);
	}
	clearTimeout(playbackReconnectTimeout);
	playbackReconnectTimeout = setTimeout(function () {
		enumerateDevices()
			.then(gotDevices2)
			.then(function () {
				resetupAudioOut();
			});
	}, 1000);
}

function handleAudioTrackEnded(event) {
    errorlog("Audio track ended unexpectedly");
	
    // If there's already a reconnection attempt in progress, don't start another one
    if (session.audioReconnectInProgress) {
        return;
    }
	
	let modalID = null;
    if (!session.cleanOutput) {
        warnUser("Your microphone disconnected. Attempting to reconnect...", 3200);
    }
    
    session.audioReconnectInProgress = true;
    
    // Wait a brief moment to ensure the device has time to be recognized again if it was unplugged/replugged
    setTimeout(function() {
        activatedPreview = false;
        grabAudio("#audioSource3", null, false);
        
        // Check if reconnection was successful after a delay
        setTimeout(function() {
            session.audioReconnectInProgress = false;
			
            closeModal(false, modalID);
				
            // Check if there are any active audio tracks after reconnection attempt
            const hasAudioTracks = session.streamSrc && 
                                   session.streamSrc.getAudioTracks && 
                                   session.streamSrc.getAudioTracks().length > 0;
            
            if (!hasAudioTracks) {
                // Reconnection failed, open settings menu
                if (!session.cleanOutput) {
                    warnUser("Failed to reconnect your microphone. Please select a different device.", 5000);
                }
                
                // Open the settings menu
                if (typeof toggleSettings === 'function') {
                    toggleSettings(true); // force show the settings
                }
            }
        }, 2000);
    }, 1000);
}


var vingesterFixed = false;
function resetupAudioOut(ele = false, forceReset = false) {
	// this re-sets ALL output devices / sources
	log("resetupAudioOut");
	if (iOS || iPad || SafariVersion || (ChromiumVersion && session.mobile)) {
		// TODO : TEST TO SEE IF THIS WORKS WITH SAFARI? it might.
		if (ele) {
			return;
		}
		for (var UUID in session.rpcs) {
			if (session.rpcs[UUID].videoElement) {
				try {
					session.rpcs[UUID].videoElement
						.pause()
						.then(() => {
							setTimeout(
								function (uuid) {
									log("win");
									try {
										session.rpcs[uuid].videoElement.play().then(() => {
											// updateIncomingAudioElement(uuid); // DO NOT DO THIS; it would cause a loop, as this updateIncomingAudioElement calls reseet
											log("toggle pause/play");
										});
									} catch (e) {
										errorlog(e);
									}
								},
								0,
								UUID
							);
						})
						.catch(errorlog);
				} catch (e) {
					warnlog(e);
				}
			}
		}
		return;
	}

	//if (isVingester){return;}

	var outputSelect = document.getElementById("outputSource") || document.getElementById("outputSource3") || false;
	var sinkSet = false;

	try {
		// function tries to get vingester working, by listening for its first tweak.
		if (!session.sink && !vingesterFixed && document.getElementById("testtone") && document.getElementById("testtone").sinkId && isVingester) {
			vingesterFixed = true; // only set the session.sink one time, as vingester on should be needing to set it one time.
			session.sink = document.getElementById("testtone").sinkId;
		}
	} catch (e) {
		errorlog(e);
	}

	if (outputSelect && outputSelect.options && outputSelect.options.length) {
		for (var i = 0; i < outputSelect.options.length; i++) {
			if (outputSelect.options[i].value == session.sink) {
				outputSelect.options[i].selected = "true";
				sinkSet = true;
			}
		}
		if (sinkSet == false) {
			if (outputSelect.options[0]) {
				outputSelect.options[0].selected = "true";
				sinkSet = outputSelect.value;
			}
		} else {
			sinkSet = session.sink;
		}
	} else {
		sinkSet = session.sink;
	}

	if (ele) {
		try {
			var eleSink = sinkSet;
			if (ele.manualSink) {
				eleSink = ele.manualSink;
				log("Manual Sink Identified");
			}
			if (eleSink) {
				if (forceReset) {
					ele.setSinkId("default")
						.then(() => {
							ele.setSinkId(eleSink)
								.then(() => {
									log("New Output Device");
								})
								.catch(error => {
									if (!Firefox) {
										warnlog(error);
									}
									// TODO:  If error, then see if I need to add mic support, and grab it if needed.
								});
						})
						.catch(error => {
							if (!Firefox) {
								errorlog(error);
							}
							ele.setSinkId(eleSink)
								.then(() => {
									log("New Output Device");
								})
								.catch(error => {
									if (!Firefox) {
										warnlog(error);
									}
									// TODO:  If error, then see if I need to add mic support, and grab it if needed.
								});
						});
				}

				ele.setSinkId(eleSink)
					.then(() => {
						log("New Output Device for self-preview");
					})
					.catch(error => {
						if (!Firefox) {
							warnlog("Need to add mic support, and grab it if needed.");
							warnlog(error);
						}
						// TODO:  If error, then see if I need to add mic support, and grab it if needed.
					});
			}
		} catch (e) {
			warnlog("can't use setsink");
		}
		log("audio sink: " + eleSink);
		return;
	}

	if (session.videoElement) {
		// this would be a preview or videosource
		try {
			var eleSink = sinkSet;
			if (session.videoElement.manualSink) {
				eleSink = session.videoElement.manualSink;
			}
			if (eleSink) {
				session.videoElement
					.setSinkId(eleSink)
					.then(() => {
						log("New Output Device for self-preview");
					})
					.catch(error => {
						if (!Firefox) {
							warnlog(error);
						}
						// TODO:  If error, then see if I need to add mic support, and grab it if needed.
					});
			}
		} catch (e) {
			warnlog("can't use setsink");
		}
	}

	for (UUID in session.rpcs) {
		try {
			if (session.rpcs[UUID].videoElement) {
				var eleSink = sinkSet;
				if (session.rpcs[UUID].videoElement.manualSink) {
					eleSink = session.rpcs[UUID].videoElement.manualSink;
				}
				if (eleSink) {
					session.rpcs[UUID].videoElement
						.setSinkId(eleSink)
						.then(() => {
							log("New Output Device for: " + UUID);
						})
						.catch(error => {
							if (!Firefox) {
								warnlog(error);
							}
							// TODO:  If error, then see if I need to add mic support, and grab it if needed.
						});
				}
			}
		} catch (e) {
			warnlog(e);
		}
	}
	log("audio sink 2: " + eleSink);
}

function obfuscateURL(input) {
	if (input.startsWith("https://obs.ninja/")) {
		input = input.replace("https://obs.ninja/", "obs.ninja/");
	} else if (input.startsWith("http://obs.ninja/")) {
		input = input.replace("http://obs.ninja/", "obs.ninja/");
	} else if (input.startsWith("obs.ninja/")) {
		input = input.replace("obs.ninja/", "obs.ninja/");
	} else if (input.startsWith("https://vdo.ninja/")) {
		input = input.replace("https://vdo.ninja/", "vdo.ninja/");
	} else if (input.startsWith("http://vdo.ninja/")) {
		input = input.replace("http://vdo.ninja/", "vdo.ninja/");
	} else if (input.startsWith("vdo.ninja/")) {
		input = input.replace("vdo.ninja/", "vdo.ninja/");
	}

	input = input.replace("&view=", "&v=");
	input = input.replace("&view&", "&v&");
	input = input.replace("?view&", "?v&");
	input = input.replace("?view=", "?v=");

	input = input.replace("&videobitrate=", "&vb=");
	input = input.replace("?videobitrate=", "?vb=");
	input = input.replace("&bitrate=", "&vb=");
	input = input.replace("?bitrate=", "?vb=");

	input = input.replace("?audiodevice=", "?ad=");
	input = input.replace("&audiodevice=", "&ad=");

	input = input.replace("?label=", "?l=");
	input = input.replace("&label=", "&l=");

	input = input.replace("?stereo=", "?s=");
	input = input.replace("&stereo=", "&s=");
	input = input.replace("&stereo&", "&s&");
	input = input.replace("?stereo&", "?s&");

	input = input.replace("?webcam&", "?wc&");
	input = input.replace("&webcam&", "&wc&");

	input = input.replace("?remote=", "?rm=");
	input = input.replace("&remote=", "&rm=");

	input = input.replace("?password=", "?p=");
	input = input.replace("&password=", "&p=");

	input = input.replace("&maxvideobitrate=", "&mvb=");
	input = input.replace("?maxvideobitrate=", "?mvb=");

	input = input.replace("&maxbitrate=", "&mvb=");
	input = input.replace("?maxbitrate=", "?mvb=");

	input = input.replace("&height=", "&h=");
	input = input.replace("?height=", "?h=");

	input = input.replace("&width=", "&w=");
	input = input.replace("?width=", "?w=");

	input = input.replace("&quality=", "&q=");
	input = input.replace("?quality=", "?q=");

	input = input.replace("&cleanoutput=", "&clean=");
	input = input.replace("?cleanoutput=", "?clean=");

	input = input.replace("&maxviewers=", "&clean=");
	input = input.replace("?maxviewers=", "?clean=");

	input = input.replace("&frameRate=", "&fr=");
	input = input.replace("?frameRate=", "?fr=");

	input = input.replace("&fps=", "&fr=");
	input = input.replace("?fps=", "?fr=");

	input = input.replace("&roomid=", "&r=");
	input = input.replace("?roomid=", "?r=");

	input = input.replace("&room=", "&r=");
	input = input.replace("?room=", "?r=");

	log(input);
	var key = "OBSNINJAFORLIFE";
	var encrypted = CryptoJS.AES.encrypt(input, key);
	var output = "https://invite.cam/" + encrypted.toString();
	return output;
}

function notifyOfScreenShare() {
	if (session.notifyScreenShare) {
		var data = {};
		data.screenShareState = session.screenShareState;
		session.sendMessage(data);
	}
}

var beforeScreenShare = null; // video
var screenShareAudioTrack = null;
async function toggleScreenShare(reload = false) {
	/// &sstype=1

	var quality = session.quality_ss;

	if (quality === false) {
		quality = session.roomid ? session.quality_room : session.quality_wb;
	}

	if (session.quality !== false) {
		quality = session.quality;
	}
	if (session.screensharequality !== false) {
		quality = session.screensharequality;
	}

	if (reload) {
		// quality = 0, audio = true, videoOnEnd = false) {
		await grabScreen(quality, true, true).then(res => {
			if (res != false) {
				session.screenShareState = true;
				pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
				notifyOfScreenShare();

				getById("screensharebutton").classList.add("green");
				getById("screensharebutton").ariaPressed = "true";
				enumerateDevices()
					.then(gotDevices2)
					.then(function () {});
			}
		});
		return;
	}
	if (session.screenShareState == false) {
		// adding a screen
		await grabScreen(quality, true, true).then(res => {
			if (res != false) {
				session.screenShareState = true;
				pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
				notifyOfScreenShare();

				//session.refreshScale();

				getById("screensharebutton").classList.add("green");
				getById("screensharebutton").ariaPressed = "true";
				enumerateDevices()
					.then(gotDevices2)
					.then(function () {});

				//if (session.videoElement.readyState!==4){
				session.videoElement.play().then(() => {
					log("start play doublecheck");
				});
				//}
				updateMixer();
				pokeIframeAPI("screen-share-state", true);
			}
		});
	} else {
		// removing a screen  . session.screenShareState already true true  /////////////////////////////////

		session.screenShareState = false;
		pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
		notifyOfScreenShare();

		if (!session.streamSrc) {
			checkBasicStreamsExist();
		}

		if (screenShareAudioTrack) {
			if (session.videoElement && session.videoElement.srcObject) {
				session.videoElement.srcObject.getAudioTracks().forEach(function (track) {
					// previous video track; saving it. Must remove the track at some point.
					if (screenShareAudioTrack.id == track.id) {
						// since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share.
						session.videoElement.srcObject.removeTrack(track);
						track.stop();
					}
				});
			}

			if (session.streamSrcClone) {
				//
				session.streamSrcClone.getAudioTracks().forEach(function (track) {
					if (screenShareAudioTrack.id == track.id) {
						// since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share.
						session.streamSrcClone.removeTrack(track);
						track.stop();
					}
				});
			}
			if (session.streamSrc) {
				session.streamSrc.getAudioTracks().forEach(function (track) {
					// previous video track; saving it. Must remove the track at some point.
					if (screenShareAudioTrack.id == track.id) {
						// since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share.
						session.streamSrc.removeTrack(track);
						track.stop();
					}
				});
			}

			session.videoElement.srcObject = outboundAudioPipeline(); // updateREnderOoutput is just for video if videoElement is already activated.
			screenShareAudioTrack = null;
			senderAudioUpdate();
		}

		var addedAlready = false;
		if (session.streamSrc) {
			session.streamSrc.getVideoTracks().forEach(function (track) {
				if (beforeScreenShare && track.id == beforeScreenShare.id) {
					addedAlready = true;
				} else {
					session.streamSrc.removeTrack(track);
					track.stop();
				}
			});
		}

		if (session.streamSrcClone) {
			session.streamSrcClone.getVideoTracks().forEach(function (track) {
				if (beforeScreenShare && track.id == beforeScreenShare.id) {
					//
				} else {
					session.streamSrcClone.removeTrack(track);
					track.stop();
				}
			});
		}

		if (session.videoElement && session.videoElement.srcObject) {
			session.videoElement.srcObject.getVideoTracks().forEach(function (track) {
				if (beforeScreenShare && track.id == beforeScreenShare.id) {
					addedAlready = true;
				} else {
					session.videoElement.srcObject.removeTrack(track);
					track.stop();
				}
			});
		}

		getById("screensharebutton").classList.remove("green");
		getById("screensharebutton").ariaPressed = "false";

		if (beforeScreenShare) {
			if (addedAlready == false) {
				session.streamSrc.addTrack(beforeScreenShare); // add back in the video track we had before we started screen sharing.  It should be NULL if we changed the video track else where (such as via the settings). #TODO:
			}
		}

		beforeScreenShare = null;
		updateRenderOutpipe(); // this syncs the video

		toggleSettings(true); // forceShow
		updateMixer();
	}
}

var ipcRenderer = false;
var ElectronDesktopCapture = false;
var windowAudioCapture = null;
var capturedAudioStream = null;

if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
	// this enables Screen Capture in Electron
	try {
		if (!ipcRenderer) {
			ipcRenderer = require("electron").ipcRenderer;
		}
		
		// Initialize WindowAudioStream helper
		windowAudioCapture = new WindowAudioStream();
		
		window.navigator.mediaDevices.getDisplayMedia = (constraints = false) => {
			return new Promise(async (resolve, reject) => {
				try {
					if (session.autostart) {
						session.autostart = false;
						if (parseInt(session.screenshare) + "" === session.screenshare) {
							var sscid = parseInt(session.screenshare) - 1;
							if (sscid < 0) {
								sscid = 0;
							}
							//ipcRenderer.sendSync('prompt', {title, val});
							const sources = await ipcRenderer.sendSync("getSources", { types: ["screen"] });

							var new_constraints = {
								audio: {
									mandatory: {
										chromeMediaSource: "desktop",
										chromeMediaSourceId: sources[sscid].id
									}
								},
								video: {
									mandatory: {
										chromeMediaSource: "desktop",
										chromeMediaSourceId: sources[sscid].id
									}
								}
							};
							if (session.audioDevice === 0) {
								new_constraints.audio = false;
							}
							try {
								if (constraints.video.width.ideal) {
									new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal;
								}
							} catch (e) {}
							try {
								if (constraints.video.height.ideal) {
									new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal;
								}
							} catch (e) {}
							try {
								if (constraints.video.frameRate.ideal) {
									new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal;
								}
							} catch (e) {}
							
							warnlog("navigator.mediaDevices.getUserMedia starting...");
							const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
							resolve(stream);
						} else if (session.screenshare && session.screenshare !== true) {
							var sscid = null;
							const sources = await ipcRenderer.sendSync("getSources", { types: ["window"] });
							for (var i = 0; i < sources.length; i++) {
								if (sources[i].name.toLowerCase().startsWith(session.screenshare.toLowerCase())) {
									// check if anythign starts with
									sscid = i;
									break;
								}
							}
							if (sscid === null) {
								sscid = 0; // grab first window if nothing.
								for (var i = 0; i < sources.length; i++) {
									if (sources[i].name.toLowerCase().includes(session.screenshare.toLowerCase())) {
										// check if something includes the string; fallback
										sscid = i;
										break;
									}
								}
							}
							///
							var new_constraints = {
								audio: {
									mandatory: {
										chromeMediaSource: "desktop",
										chromeMediaSourceId: sources[sscid].id
									}
								},
								video: {
									mandatory: {
										chromeMediaSource: "desktop",
										chromeMediaSourceId: sources[sscid].id
									}
								}
							};

							if (session.audioDevice === 0) {
								new_constraints.audio = false;
							}
							try {
								if (constraints.video.width.ideal) {
									new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal;
								}
							} catch (e) {}
							try {
								if (constraints.video.height.ideal) {
									new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal;
								}
							} catch (e) {}
							try {
								if (constraints.video.frameRate.ideal) {
									new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal;
								}
							} catch (e) {}
							
							warnlog("navigator.mediaDevices.getUserMedia starting...");
							const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
							resolve(stream);
						} else {
							var sscid = 0;
							const sources = await ipcRenderer.sendSync("getSources", { types: ["screen"] });
							///
							var new_constraints = {
								audio: {
									mandatory: {
										chromeMediaSource: "desktop",
										chromeMediaSourceId: sources[sscid].id
									}
								},
								video: {
									mandatory: {
										chromeMediaSource: "desktop",
										chromeMediaSourceId: sources[sscid].id
									}
								}
							};
							if (session.audioDevice === 0) {
								new_constraints.audio = false;
							}
							try {
								if (constraints.video.width.ideal) {
									new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal;
								}
							} catch (e) {}
							try {
								if (constraints.video.height.ideal) {
									new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal;
								}
							} catch (e) {}
							try {
								if (constraints.video.frameRate.ideal) {
									new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal;
								}
							} catch (e) {}
							warnlog(new_constraints);
							
							warnlog("navigator.mediaDevices.getUserMedia starting...");
							const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
							resolve(stream);
						}
					} else {
						// Get both sources and window audio
						const sources = await ipcRenderer.sendSync("getSources", { types: ["screen", "window"] });
						
						// Get window list with audio capabilities
						const windowsWithAudio = await window.electronApi.getWindowList();
						
						// Create a map of window IDs to process IDs for matching
						const windowAudioMap = {};
						windowsWithAudio.forEach(win => {
							windowAudioMap[win.id] = {
								processId: win.processId,
								title: win.title
							};
						});
						
						const selectionElem = document.createElement("div");
						selectionElem.classList = "desktop-capturer-selection";

						if (session.screenshareVideoOnly) {
							selectionElem.innerHTML = `
						<div class="desktop-capturer-selection__scroller">
						  <ul class="desktop-capturer-selection__list">
							${sources
								.map(
									({ id, name, thumbnail, display_id, appIcon }) => `
							  <li class="desktop-capturer-selection__item">
								<button class="desktop-capturer-click desktop-capturer-selection__btn" data-id="${id}" title="${name}">
								  <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" />
								  <span class="desktop-capturer-selection__name">${name}</span>
								</button>
							  </li>
							`
								)
								.join("")}
							<button id="cancelscreenshare" style="margin: 10px; background-color: #F88; width: 100px;"><i class="las la-window-close" style="font-size:40px;"></i><br />Cancel</button>
						  </ul>
						</div>
					  `;
						} else {
							selectionElem.innerHTML = `
						<div class="desktop-capturer-selection__scroller">
						  <ul class="desktop-capturer-selection__list">
							${sources
								.map(
									({ id, name, thumbnail, display_id, appIcon }) => {
										// Find if this window has audio capability
										const hasAudioCapability = Object.values(windowAudioMap).some(win => 
											win.title.includes(name) || name.includes(win.title)
										);
										
										return `
							  <li class="desktop-capturer-selection__item">
								<button class="desktop-capturer-click desktop-capturer-selection__btn" data-id="${id}" data-name="${name}" title="${name}">
								  <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" />
								  <span class="desktop-capturer-selection__name">${name}</span>
								  ${hasAudioCapability ? 
										`<div class="desktop-capturer-selection__audio-option">
											<label title="Capture window audio">
												<input type="checkbox" class="capture-window-audio" data-source-name="${name}">
												<span><i class="las la-volume-up"></i> Capture audio</span>
											</label>
										</div>` : ''}
								</button>
							  </li>
							`})
								.join("")}
							<div id="alsoCaptureAudioParent1" style="text-align: center;margin: auto 5px;font-size: 120%;">
								<i class="las la-music" style="font-size:40px;"></i><br />Include System Audio<br />
								<input id="alsoCaptureAudio" style="width:20px;height:20px;margin-top: 10px;" type="checkbox" checked>
							</div>
							<div id="alsoCaptureAudioParent2" style="text-align: center;margin: auto 5px;font-size: 120%;display:none;">
								<i class="las la-music" style="font-size:40px;"></i><br />Audio capture not <br />supported on macOS
							</div>
							<button id="captureDesktopAudio" class="desktop-capturer-click" style="margin: 10px;">
								<i class="las la-music" style="font-size:40px;"></i><br />Capture ONLY<br />Desktop Audio
							</button>
							<button id="cancelscreenshare" style="margin: 10px; background-color: #F88; width: 100px;">
								<i class="las la-window-close" style="font-size:40px;"></i><br />Cancel
							</button>
						  </ul>
						</div>
					  `;
						}
						document.body.appendChild(selectionElem);

						if (macOS) {
							getById("captureDesktopAudio").style.display = "none";
							getById("alsoCaptureAudio").checked = false;
							getById("alsoCaptureAudioParent1").style.display = "none";
							getById("alsoCaptureAudioParent2").style.display = "inline-block";
						}

						document.getElementById("cancelscreenshare").addEventListener("click", async () => {
							selectionElem.remove();
							reject(null);
						});
						
						document.querySelectorAll(".desktop-capturer-click").forEach(button => {
							button.addEventListener("click", async () => {
								try {
									if (button.id == "captureDesktopAudio") {
										var new_constraints = {
											audio: {
												mandatory: {
													chromeMediaSource: "desktop"
												}
											},
											video: {
												mandatory: {
													chromeMediaSource: "desktop"
												}
											}
										};
										new_constraints.video.mandatory.maxFrameRate = 1;
										warnlog(new_constraints);
										
										warnlog("navigator.mediaDevices.getUserMedia starting...");
										const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
										if (stream.getVideoTracks().length) {
											var track = stream.getVideoTracks()[0];
											stream.removeTrack(stream.getVideoTracks()[0]);
											track.stop();
										}
										resolve(stream);
										selectionElem.remove();
									} else {
										var audioStream = false;
										// Check if we should capture system audio
										if (getById("alsoCaptureAudio").checked) {
											var new_constraints = {
												audio: {
													mandatory: {
														chromeMediaSource: "desktop"
													}
												},
												video: {
													mandatory: {
														chromeMediaSource: "desktop"
													}
												}
											};
											new_constraints.video.mandatory.maxFrameRate = 1;
											warnlog(new_constraints);
											
											warnlog("navigator.mediaDevices.getUserMedia starting...");
											audioStream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
											if (audioStream.getVideoTracks().length) {
												var track = audioStream.getVideoTracks()[0];
												audioStream.removeTrack(audioStream.getVideoTracks()[0]);
												track.stop();
											}
										}

										// Get the source ID for the selected window
										const id = button.getAttribute("data-id");
										const source = sources.find(source => source.id === id);
										if (!source) {
											throw new Error(`Source with id ${id} does not exist`);
										}
										
										// Check if we should capture window-specific audio
										const sourceName = button.getAttribute("data-name");
										const audioCheckbox = button.querySelector('.capture-window-audio');
										let windowAudioId = null;
										
										// If window-specific audio is requested
										if (audioCheckbox && audioCheckbox.checked) {
											// Find the window by name
											for (const win of windowsWithAudio) {
												if (win.title.includes(sourceName) || sourceName.includes(win.title)) {
													windowAudioId = win.id;
													break;
												}
											}
											
											// Start capturing audio for the specific window
											if (windowAudioId) {
												try {
													// Stop any previous capture
													if (capturedAudioStream) {
														await windowAudioCapture.stop();
														capturedAudioStream = null;
													}
													
													// Start the capture
													capturedAudioStream = await windowAudioCapture.start(windowAudioId);
													console.log("Window audio capture started for: " + sourceName);
												} catch (e) {
													console.error("Failed to capture window audio:", e);
												}
											}
										}
										
										// Setup normal screen capture
										var new_constraints = {
											audio: false,
											video: {
												mandatory: {
													chromeMediaSource: "desktop",
													chromeMediaSourceId: source.id
												}
											}
										};
										try {
											if (constraints.video.width.ideal) {
												new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal;
											}
										} catch (e) {}
										try {
											if (constraints.video.height.ideal) {
												new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal;
											}
										} catch (e) {}
										try {
											if (constraints.video.frameRate.ideal) {
												new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal;
											}
										} catch (e) {}
										warnlog(new_constraints);
										
										warnlog("navigator.mediaDevices.getUserMedia starting...");
										const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);

										// Add system audio if available
										if (audioStream && audioStream.getAudioTracks().length) {
											stream.addTrack(audioStream.getAudioTracks()[0]);
										}
										
										// Add window-specific audio if available
										if (capturedAudioStream && capturedAudioStream.getAudioTracks().length) {
											stream.addTrack(capturedAudioStream.getAudioTracks()[0]);
										}

										resolve(stream);
										selectionElem.remove();
									}
								} catch (err) {
									errorlog("Error selecting desktop capture source:", err);
									reject(err);
								}
							});
						});
					}
				} catch (err) {
					errorlog("Error displaying desktop capture sources:", err);
					reject(err);
				}
			});
		};
		ElectronDesktopCapture = true;
	} catch (e) {
		warnlog("Couldn't load electron's screen capture. Elevate the app's permission to allow it (right-click?)");
	}
	
	try {
		if (!isIFrame){
			const draggableCSS = `
			  #electronDragZone, #header { -webkit-app-region: drag; }
			`;
			const nonDraggableCSS = `
			  #popupSelector, a, input, button, #head1, #head4, #head5,
			  .close, select, button { -webkit-app-region: no-drag; }
			`;
			const dragStyle = document.createElement('style');
			dragStyle.textContent = draggableCSS;
			document.head.appendChild(dragStyle);
			const noDragStyle = document.createElement('style');
			noDragStyle.textContent = nonDraggableCSS;
			document.head.appendChild(noDragStyle);
			getById("electronDragZone").style.display = "unset";
		}
	} catch(e) {
	  console.error("Error applying Electron/OBS CSS fixes:", e);
	}
	
	// Add styles for audio capture checkboxes
	const audioCheckboxStyle = document.createElement('style');
	audioCheckboxStyle.textContent = `
		.desktop-capturer-selection__audio-option {
			margin-top: 5px;
			font-size: 12px;
			display: flex;
			align-items: center;
			justify-content: center;
		}
		.desktop-capturer-selection__audio-option label {
			display: flex;
			align-items: center;
			cursor: pointer;
		}
		.desktop-capturer-selection__audio-option input {
			margin-right: 5px;
		}
		.desktop-capturer-selection__audio-option span {
			display: flex;
			align-items: center;
		}
		.desktop-capturer-selection__audio-option i {
			margin-right: 3px;
		}
	`;
	document.head.appendChild(audioCheckboxStyle);
}

async function grabScreen(quality = 0, audio = true, videoOnEnd = false) {
	if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
		if (!session.cleanOutput) {
			setTimeout(function () {
				if (iOS || iPad) {
					warnUser(getTranslation("ios-no-screen-share"), false, false);
				} else if (session.mobile) {
					warnUser(getTranslation("mobile-no-screen-share"), false, false);
				} else if (Firefox && !session.mobile) {
					warnUser(getTranslation("no-screen-share-supported-firefox"), false, false);
				} else {
					warnUser(getTranslation("no-screen-share-supported"), false, false);
				}
			}, 1);
		}
		return false;
	}

	if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
		if (!ElectronDesktopCapture) {
			if (!session.cleanOutput) {
				warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)");
			}
			return false;
		}
	}

	var video = {};

	if (quality == -1) {
		// unlocked capture resolution
	} else if (quality == 0) {
		video.width = {
			ideal: 1920
		};
		video.height = {
			ideal: 1080
		};
	} else if (quality == 1) {
		video.width = {
			ideal: 1280
		};
		video.height = {
			ideal: 720
		};
	} else if (quality == 2) {
		video.width = {
			ideal: 640
		};
		video.height = {
			ideal: 360
		};
	} else if (quality >= 3) {
		// lowest
		video.width = {
			ideal: 320
		};
		video.height = {
			ideal: 180
		};
	}

	if (session.width) {
		video.width = {
			ideal: session.width
		};
	}
	if (session.height) {
		video.height = {
			ideal: session.height
		};
	}

	var constraints = {
		// this part is a bit annoying. Do I use the same settings?  I can add custom setting controls here later
		audio: {
			echoCancellation: false, // For screen sharing, we want it off by default.
			autoGainControl: false,
			noiseSuppression: false
		},
		video: video
		//,cursor: {exact: "none"}
	};

	try {
		let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
		if (supportedConstraints.cursor) {
			if (session.screensharecursor) {
				constraints.video.cursor = ["always", "motion"];
			} else {
				constraints.video.cursor = "never";
			}
		}
		if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) {
			constraints.audio.suppressLocalAudioPlayback = true;
		}
		if (session.preferCurrentTab) {
			constraints.preferCurrentTab = true;
		}
		if (session.selfBrowserSurface) {
			constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include
		}
		if (session.surfaceSwitching) {
			constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include
		}
		if (session.systemAudio) {
			constraints.systemAudio = session.systemAudio; // exclude or include
		}
		if (session.displaySurface && supportedConstraints.displaySurface) {
			constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser
		}
	} catch (e) {
		warnlog("navigator.mediaDevices.getSupportedConstraints() not supported");
	}

	if (session.echoCancellation === true) {
		constraints.audio.echoCancellation = true;
	}
	if (session.autoGainControl === true) {
		constraints.audio.autoGainControl = true;
	}
	if (session.noiseSuppression === true) {
		constraints.audio.noiseSuppression = true;
	}
	if (session.voiceIsolation === true){
		constraint.audio.voiceIsolation = true;
	}
	if (audio == false) {
		constraints.audio = false;
	}

	var overrideFramerate = false;
	if (session.frameRate !== false && session.maxframeRate != false) {
		overrideFramerate = session.frameRate;
		constraints.video.frameRate = {
			ideal: session.maxframeRate,
			max: session.maxframeRate
		};
	} else if (session.frameRate !== false) {
		constraints.video.frameRate = session.frameRate;
	} else if (session.maxframeRate != false) {
		constraints.video.frameRate = {
			ideal: session.maxframeRate,
			max: session.maxframeRate
		};
	} else {
		constraints.video.frameRate = {
			ideal: 60
		};
	}

	if (session.screenshareVideoOnly) {
		constraints.audio = false;
	}

	if (session.forceAspectRatio) {
		// await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
		if (constraints.video && constraints.video !== true) {
			constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) };

			if (constraints.video.width && !session.width) {
				delete constraints.video.width;
			} else if (constraints.video.height && !session.height) {
				delete constraints.video.height;
			}
		}
	}

	if (constraints.video !== false && Object.keys(constraints.video).length == 0) {
		constraints.video = true;
	}

	var wasDisabled = true;

	return navigator.mediaDevices
		.getDisplayMedia(constraints)
		.then(async function (stream) {
			log("adding video tracks 2245");

			try {
				var constraint = {};
				if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) {
					constraint.aspectRatio = parseFloat(session.forceAspectRatio);
				} else if (session.forceScreenShareAspectRatio) {
					constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio);
				}
				if (overrideFramerate) {
					constraint.frameRate = overrideFramerate;
				}
				if (Object.keys(constraint).length) {
					await stream.getVideoTracks()[0].applyConstraints({
						advanced: [constraint]
					});
					log({
						advanced: [constraint]
					});
				}
			} catch (e) {
				errorlog(e);
			}

			try {
				if (session.streamSrc) {
					session.streamSrc.getVideoTracks().forEach(function (track) {
						//track.stop();
						beforeScreenShare = track;
						session.streamSrc.removeTrack(track);
						wasDisabled = false; //
						log("stopping video track");
					});
					if (session.streamSrcClone) {
						session.streamSrcClone.getVideoTracks().forEach(function (track) {
							session.streamSrcClone.removeTrack(track);
							track.stop();
						});
					}
					if (session.videoElement && session.videoElement.srcObject) {
						session.videoElement.srcObject.getVideoTracks().forEach(function (track) {
							//track.stop();
							wasDisabled = false;
							session.videoElement.srcObject.removeTrack(track);
							log("stopping video track 2");
						});
					} else {
						checkBasicStreamsExist();
					}
				} else {
					checkBasicStreamsExist(); // create srcObject + videoElement
				}
			} catch (e) {
				warnlog(e);
			}

			try {
				stream.getVideoTracks()[0].onended = function (e) {
					// if screen share stops,
					warnlog(e);
					
					// Clean up any window-specific audio capture
					if (windowAudioCapture && capturedAudioStream) {
						windowAudioCapture.stop();
						capturedAudioStream = null;
					}
					
					if (session.streamSrc) {
						session.streamSrc.getVideoTracks().forEach(function (track) {
							session.streamSrc.removeTrack(track);
							track.stop();
							log("stopping video track 3");

							if (beforeScreenShare && beforeScreenShare.id == track.id) {
								beforeScreenShare.stop();
								beforeScreenShare = null;
							}
						});
					}

					if (session.streamSrcClone) {
						session.streamSrcClone.getVideoTracks().forEach(function (track) {
							session.streamSrcClone.removeTrack(track);
							track.stop();
						});
					}

					if (session.videoElement && session.videoElement.srcObject) {
						session.videoElement.srcObject.getVideoTracks().forEach(function (track) {
							session.videoElement.srcObject.removeTrack(track);
							track.stop();
							log("stopping video track 4");
						});
					} else {
						//session.videoElement.srcObject = createMediaStream();
						session.videoElement.srcObject = outboundAudioPipeline();
					}

					if (screenShareAudioTrack) {
						if (session.streamSrc) {
							session.streamSrc.getAudioTracks().forEach(function (track) {
								// previous video track; saving it. Must remove the track at some point.
								if (screenShareAudioTrack.id == track.id) {
									// since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share.
									session.streamSrc.removeTrack(track);
									track.stop();
								}
							});
						}
						if (session.streamSrcClone) {
							session.streamSrcClone.getAudioTracks().forEach(function (track) {
								// previous video track; saving it. Must remove the track at some point.
								if (screenShareAudioTrack.id == track.id) {
									// since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share.
									session.streamSrcClone.removeTrack(track);
									track.stop();
								}
							});
						}
						screenShareAudioTrack = null;
						senderAudioUpdate();
					}

					session.screenShareState = false;
					pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
					notifyOfScreenShare();

					getById("screensharebutton").classList.remove("green");
					getById("screensharebutton").ariaPressed = "false";

					if (videoOnEnd == true) {
						if (beforeScreenShare) {
							session.streamSrc.addTrack(beforeScreenShare); // updateRenderOutpipe
							beforeScreenShare = null;
						}
						updateRenderOutpipe();
						toggleSettings(true); // forceshow
					} else {
						//session.refreshScale(); // since updateREnderOutput already has htis.
					}
					updateMixer();
				};
			} catch (e) {
				log("No Video selected; screensharing?");
			}

			stream.getTracks().forEach(function (track) {
				addScreenDevices(track);
				session.streamSrc.addTrack(track, stream); // Lets not add the audio to this preview; echo can be annoying
			});
			updateRenderOutpipe();

			if (wasDisabled && stream.getVideoTracks().length && !session.videoMuted) {
				var msg = {};
				msg.videoMuted = session.videoMuted;
				session.sendMessage(msg);
			}

			if (stream.getAudioTracks().length) {
				screenShareAudioTrack = stream.getAudioTracks()[0];
				senderAudioUpdate();
			}

			session.applySoloChat(); // mute streams that should be muted if a director
			session.applyIsolatedChat();

			applyMirror(true);

			return true;
		})
		.catch(function (err) {
			errorlog(err);
			errorlog(err.name);
			if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") {
				// User Stopped it.
				if (macOS) {
					warnUser(getTranslation("screen-permissions-denied"), false, false);
				}
			} else {
									if (audio == true) {
					if (err.name == "NotReadableError") {
						if (!session.cleanOutput) {
							warnUser(getTranslation("change-audio-output-device"), false, false);
						}
						setTimeout(function () {
							grabScreen(quality, false);
						}, 1);
						return false;
					} else {
						setTimeout(function () {
							grabScreen(quality, false);
						}, 1);
					}
				}
				if (!session.cleanOutput) {
					setTimeout(
						function (e) {
							errorlog(e);
						},
						1,
						err
					); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported
				}
			}
			return false;
		});
}

function toggleBufferSettings(UUID) {
	getById("bufferSettings").dataset.UUID = UUID;
	toggle(getById("bufferSettings"));
	if (getById("bufferSettings").style.display == "none") {
		getById("modalBackdrop").innerHTML = ""; // Delete modal
		getById("modalBackdrop").remove();
	} else {
		getById("modalBackdrop").innerHTML = ""; // Delete modal
		getById("modalBackdrop").remove();
		zindex = 25;
		getById("bufferSettings").style.zIndex = 25;
		var modalTemplate = `<div id="modalBackdrop" style="z-index:24;background-color:#0000;"></div>`;
		document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end
		document.getElementById("modalBackdrop").addEventListener("click", toggleBufferSettings);

		var buffer = session.rpcs[UUID].buffer;
		if (buffer === false) {
			buffer = session.buffer || 0;
		}
		getById("bufferSettings")
			.querySelectorAll("input")
			.forEach(ele => {
				ele.value = parseInt(buffer);
				ele.title = ele.value + " ms";
				//getById("bufferSliderValue").innerText = ele.title
				ele.onchange = function (e) {
					session.rpcs[UUID].buffer = parseInt(e.target.value);
					//getById("bufferSliderValue").innerText = session.rpcs[UUID].buffer + " ms";
					getById("bufferSettings")
						.querySelectorAll("input")
						.forEach(ele2 => {
							if (ele2 !== e.target) {
								ele2.value = parseInt(e.target.value);
							}
							ele2.title = parseInt(e.target.value) + " ms";
						});
					playoutdelay(UUID); // trigger
				};
				ele.onkeyup = function (e) {
					if (e.key === "Enter") {
						session.rpcs[UUID].buffer = parseInt(e.target.value);
						//getById("bufferSliderValue").innerText = session.rpcs[UUID].buffer + " ms";
						getById("bufferSettings")
							.querySelectorAll("input")
							.forEach(ele2 => {
								if (ele2 !== e.target) {
									ele2.value = parseInt(e.target.value);
								}
								ele2.title = parseInt(e.target.value) + " ms";
							});
						playoutdelay(UUID); // trigger
					}
				};
				ele.oninput = function (e) {
					getById("bufferSettings")
						.querySelectorAll("input")
						.forEach(ele2 => {
							if (ele2 !== e.target) {
								ele2.value = parseInt(e.target.value);
							}
							ele2.title = parseInt(e.target.value) + " ms";
						});
				};
			});
	}
}

function toggleRoomSettings() {
	toggle(getById("roomSettings"));
	if (getById("roomSettings").style.display == "none") {
		//getById("modalBackdrop").innerHTML = ''; // Delete modal
		//getById("modalBackdrop").remove();
	} else {
		//getById("modalBackdrop").innerHTML = ''; // Delete modal
		//getById("modalBackdrop").remove();
		zindex = 25;
		getById("roomSettings").style.zIndex = 25;
		var modalTemplate = `<div id="modalBackdrop" style="z-index:24"></div>`;
		//	document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end
		//	document.getElementById("modalBackdrop").addEventListener("click", toggleRoomSettings);
		getById("trbSettingInput").value = session.totalRoomBitrate;
		getById("trbSettingInputManual").value = session.totalRoomBitrate;
		getById("trbSettingInputFeedback").innerHTML = session.totalRoomBitrate;

		if (session.limitTotalBitrate !== false) {
			getById("ltbSettingInputManual").value = session.limitTotalBitrate;
			getById("ltbSettingInput").value = session.limitTotalBitrate;
			getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled";
		}
	}
}

function changeLTB(ele) {
	session.limitTotalBitrate = parseInt(ele.value);

	getById("ltbSettingInputManual").value = session.limitTotalBitrate;
	getById("ltbSettingInput").value = session.limitTotalBitrate;
	getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled";

	pokeIframeAPI("limit-total-bitrate", session.limitTotalBitrate);
	session.limitTotalBitrateGuests();
}

function changeTRB(ele) {
	session.totalRoomBitrate = parseInt(ele.value);
	var msg = {};
	msg.directorSettings = {};
	msg.directorSettings.totalRoomBitrate = session.totalRoomBitrate;
	session.sendMessage(msg);
	pokeIframeAPI("total-room-bitrate", session.totalRoomBitrate);
}

function sendMediaDevices(UUID) {
	enumerateDevices().then(function (deviceInfos) {
		var data = {};
		data.UUID = UUID;
		data.mediaDevices = deviceInfos;
		session.sendMessage(data, data.UUID);
	});
}

function changeVideoDevice(index, quality = 0) {
	enumerateDevices()
		.then(gotDevices2)
		.then(function () {
			activatedPreview = false;
			document.getElementById("videoSource3").selectedIndex = index + "";
			grabVideo(quality, "videosource", "#videoSource3");
		});
}

function changeAudioDevice(index) {
	enumerateDevices()
		.then(gotDevices2)
		.then(function () {
			activatedPreview = false;
			var audioSelect = document.getElementById("audioSource3").querySelectorAll("input");
			for (var i = 0; i < audioSelect.length; i++) {
				audioSelect[i].checked = false;
			}
			audioSelect[index - 1].checked = true;
			grabAudio("#audioSource3");
		});
}

function changeVideoDeviceById(deviceId, UUID = false) {
	enumerateDevices()
		.then(gotDevices2)
		.then(function () {
			var opts = document.getElementById("videoSource3").options;
			var index = false;
			for (var opt, j = 0; (opt = opts[j]); j++) {
				if (opt.value == deviceId) {
					index = j;
					break;
				}
			}
			if (index !== false) {
				if (document.getElementById("videoSource3").selectedIndex === index) {
					//  this is just refreshing the device.
					activatedPreview = false;
					grabVideo(0, "videosource", "#videoSource3", UUID);
				} else if (UUID && !session.consent) {
					window.focus();
					confirmAlt("Allow the director to change your video device to:\n\n" + opts[index].text + " ?").then(res => {
						if (res) {
							document.getElementById("videoSource3").selectedIndex = index;
							activatedPreview = false;
							grabVideo(0, "videosource", "#videoSource3", UUID);
						} else {
							try {
								var data = {};
								data.UUID = UUID;
								data.rejected = "changeCamera";
								session.sendMessage(data, data.UUID);
							} catch (e) {}
						}
					});
				} else {
					document.getElementById("videoSource3").selectedIndex = index;
					activatedPreview = false;
					grabVideo(0, "videosource", "#videoSource3", UUID);
				}
			}
		});
}

function changeAudioDeviceById(deviceId, UUID = false) {
	enumerateDevices()
		.then(gotDevices2)
		.then(function () {
			var audioSelect = document.getElementById("audioSource3").querySelectorAll("input");
			var matched = false;
			var exists = false;
			for (var i = 0; i < audioSelect.length; i++) {
				if (audioSelect[i].value == deviceId) {
					exists = true;
					if (audioSelect[i].checked) {
						matched = true;
					}
				}
			}

			if (exists) {
				if (matched) {
					//  this is just refreshing the device.
					activatedPreview = false;
					//grabAudio("#audioSource3", UUID);    
					grabAudio("#audioSource3", null, false, UUID);				
				} else if (UUID && !session.consent) {
					window.focus();
					confirmAlt("Allow the director to change your audio mic source?").then(res => {
						if (res) {
							//	enumerateDevices().then(gotDevices2).then(function() {
							var audioSelect = document.getElementById("audioSource3").querySelectorAll("input");
							for (var i = 0; i < audioSelect.length; i++) {
								if (audioSelect[i].value == deviceId) {
									audioSelect[i].checked = true;
								} else {
									audioSelect[i].checked = false;
								}
							}
							activatedPreview = false;
							grabAudio("#audioSource3", null, false, UUID);		
							//	});
						} else {
							try {
								var data = {};
								data.UUID = UUID;
								data.rejected = "changeMicrophone";
								session.sendMessage(data, data.UUID);
							} catch (e) {}
						}
					});
				} else {
					//enumerateDevices().then(gotDevices2).then(function() {
					var audioSelect = document.getElementById("audioSource3").querySelectorAll("input");
					for (var i = 0; i < audioSelect.length; i++) {
						if (audioSelect[i].value == deviceId) {
							audioSelect[i].checked = true;
						} else {
							audioSelect[i].checked = false;
						}
					}
					activatedPreview = false;
					grabAudio("#audioSource3", null, false, UUID);		
					//	});
				}
			}
		});
}

function changeAudioOutputDeviceById(deviceId, UUID = false) {
	// remote control of the speaker output.
	warnlog(deviceId);
	if (document.getElementById("outputSource3")) {
		enumerateDevices()
			.then(gotDevices2)
			.then(function () {
				var index = false;
				if (document.getElementById("outputSource3")) {
					var opts = document.getElementById("outputSource3").options;
					for (var opt, j = 0; (opt = opts[j]); j++) {
						if (opt.value == deviceId) {
							index = j;
							break;
						}
					}
				}
				if (index !== false) {
					if (document.getElementById("outputSource3").selectedIndex === index) {
						//  this is just refreshing the device.
						session.sink = deviceId;
						saveSettings();
						resetupAudioOut();
					} else if (UUID && !session.consent) {
						// UUID just lets us inform the requester
						window.focus();
						confirmAlt("Allow the director to change your audio's speaker to:\n\n" + opts[index].text + " ?").then(res => {
							if (res) {
								if (index !== false) {
									document.getElementById("outputSource3").selectedIndex = index;
								}
								session.sink = deviceId;
								saveSettings();
								resetupAudioOut();
								var data = {};
								data.UUID = UUID;
								sendMediaDevices(data.UUID);
								session.sendMessage(data, data.UUID);
							} else {
								try {
									var data = {};
									data.UUID = UUID;
									data.rejected = "changeSpeaker";
									session.sendMessage(data, data.UUID);
								} catch (e) {}
							}
						});
					} else {
						if (index !== false) {
							document.getElementById("outputSource3").selectedIndex = index;
						}
						session.sink = deviceId;
						saveSettings();
						resetupAudioOut();
					}
				}
			});
	} else {
		session.sink = deviceId;
		saveSettings();
		resetupAudioOut();
	}
}

function checkBasicStreamsExist() {
	log("checkBasicStreamsExist()");
	if (!session.streamSrc) {
		session.streamSrc = createMediaStream();
	}
	if (!session.videoElement) {
		if (document.getElementById("videosource")) {
			session.videoElement = document.getElementById("videosource");
		} else if (document.getElementById("previewWebcam")) {
			session.videoElement = document.getElementById("previewWebcam");
		} else {
			session.videoElement = createVideoElement();
		}

		session.videoElement.addEventListener(
			"playing",
			e => {
				resetupAudioOut(session.videoElement, true);
			},
			{ once: true }
		);

		session.videoElement.onpause = event => {
			// prevent things from pausing; human or other
			if (!(event.ctrlKey || event.metaKey)) {
				log("Video paused; auto playing");
				event.currentTarget
					.play()
					.then(_ => {
						log("playing 10");
					})
					.catch(warnlog);
			}
		};

		session.videoElement.addEventListener("error", function (event) {
			errorlog("video error");
			errorlog(event);
			setTimeout(function () {
				if (session.videoElement) {
					log("Trying to re-load local preview, as it may have crashed");
					session.videoElement.load();
				}
			}, 1200);
		});

		//session.videoElement.addEventListener('loadedmetadata', function(event) {
		//	log("loadedmetadata");
		//	log(event);
		//});
	}
	session.videoElement.srcObject = outboundAudioPipeline();
	toggleMute(true);
	return session.videoElement;
}

var getUserMediaRequestID = 0;
var grabVideoUserMediaTimeout = null;
var grabVideoTimer = null;

async function grabVideo(quality = 0, eleName = "previewWebcam", selector = "select#videoSourceSelect", callback = false) {
	if (activatedPreview == true) {
		log("activated preview return 2");
		return;
	}

	if (session.miconly) {
		return;
	}

	activatedPreview = true;
	log("Grabbing video: " + quality);
	if (grabVideoTimer) {
		clearTimeout(grabVideoTimer);
	}
	log("element:" + eleName);

	var wasDisabled = true;
	try {
		if (session.streamSrc) {
			if (session.canvasWebGL) {
				session.canvasWebGL.remove();
				session.canvasWebGL = null;
			}

			if (session.canvasSource) {
				session.canvasSource.srcObject.getTracks().forEach(function (trk) {
					session.canvasSource.srcObject.removeTrack(trk);
					trk.stop();
					wasDisabled = false;
				});
			}
			if (session.streamSrc) {
				session.streamSrc.getVideoTracks().forEach(function (track) {
					session.streamSrc.removeTrack(track);
					track.stop();
					wasDisabled = false;
				});
			}
			if (session.streamSrcClone) {
				session.streamSrcClone.getVideoTracks().forEach(function (track) {
					session.streamSrcClone.removeTrack(track);
					track.stop();
				});
			}
		} else {
			checkBasicStreamsExist();
			log("CREATE NEW STREAM");
		}

		if (session.videoElement && session.videoElement.srcObject) {
			session.videoElement.srcObject.getVideoTracks().forEach(function (track) {
				session.videoElement.srcObject.removeTrack(track);
				track.stop();
				session.videoElement.load();
				wasDisabled = false;
			});
		} else {
			checkBasicStreamsExist();
		}
	} catch (e) {
		errorlog(e);
	}

	session.videoElement.controls = session.showControls || false;

	log("selector: " + selector);
	var videoSelect = document.querySelector(selector); // document.querySelector("videoSource3").value == "ZZZ"
	log(videoSelect);
	var mirror = false;
	getById("cameraTip1").classList.add("hidden");

	if (!videoSelect || videoSelect.value == "ZZZ") {
		// if there is no video, or if manually set to audio ready, then do this step.

		clearTimeout(grabVideoUserMediaTimeout);
		getUserMediaRequestID += 1;

		warnlog("ZZZ SET - so no VIDEO");
		SelectedVideoInputDevices = [];
		saveSettings();

		if (session.avatar && session.avatar.ready) {
			updateRenderOutpipe();
		} else if (session.mobile && !document.getElementById("keepAlivePlayer") && needsLegacyWakeLock()) {  // OBSOLETE since we now have "WAKE LOCK" API used.
			// keep alive player doens't exist
			setInterval(function () {
				if (document.getElementById("keepAlivePlayer") && session.streamSrc.getVideoTracks().length) {
					getById("keepAlivePlayer").remove();
				} else if (!document.getElementById("keepAlivePlayer")) {
					let fakeElement = document.createElement("video");
					fakeElement.autoplay = true;
					fakeElement.loop = true;
					fakeElement.muted = true;
					fakeElement.src = "./media/micro.mp4";
					fakeElement.style.width = "1px";
					fakeElement.style.height = "1px";
					fakeElement.controls = false;
					fakeElement.id = "keepAlivePlayer";
					getById("main").appendChild(fakeElement);
				}
			}, 4000);
		}

		if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) {
			if (session.autostart) {
				publishWebcam(); // no need to mirror as there is no video...
				return;
			} else {
				log("4462");
				updateStats();
				if (document.getElementById("gowebcam")) {
					document.getElementById("gowebcam").dataset.ready = "true";
					if (document.getElementById("gowebcam").dataset.audioready == "true") {
						document.getElementById("gowebcam").disabled = false;
						//document.getElementById("gowebcam").innerHTML = getTranslation("start");
						miniTranslate(document.getElementById("gowebcam"), "start");
						document.getElementById("gowebcam").focus();
					}
				}
			}
		} else {
			// If they disabled the video but not in preview mode; but actualy live. We will want to remove the stream from the publishing
			// we don't want to do this otherwise, as we are "replacing" the track in other cases.
			// this does cause a problem, as previous bitrate settings & resolutions might not be applied if switched back....  must test

			if (session.avatar && session.avatar.ready) {
				updateRenderOutpipe();
				return;
			}

			if (session.chunked) {
				for (UUID in session.pcs) {
					session.chunkedStream(UUID); // make sure we check that this connection allows video / audio
				}
				// return;
			}
			try {
				var miscSenders = [];

				if (session.whipOut && session.whipOut.getSenders) {
					miscSenders.push(session.whipOut);
				}

				miscSenders.forEach(dataRTC => {
					if (dataRTC && dataRTC.getSenders) {
						dataRTC.getSenders().forEach(sender => {
							// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
							if (sender.track && sender.track.kind == "video") {
								var trk = getWhipOutCanvasTrack(dataRTC);
								if (session.screenShareState && session.screenshareContentHint && trk.kind === "video") {
									try {
										trk.contentHint = session.screenshareContentHint;
									} catch (e) {
										errorlog(e);
									}
								} else if (session.contentHint && trk.kind === "video") {
									try {
										trk.contentHint = session.contentHint;
									} catch (e) {
										errorlog(e);
									}
								}
								try {
									sender.replaceTrack(trk); // replace may not be supported by all browsers.  eek.
								} catch (e) {
									errorlog(e);
								}
							}
						});
					}
				});
			} catch (e) {
				errorlog(e);
			}

			for (UUID in session.pcs) {
				if ("realUUID" in session.pcs[UUID]) {
					continue;
				} // do not apply to screen shares.
				
				if (session.chunked && session.pcs[UUID].allowChunked){
					continue;
				}
				
				// for any connected peer, update the video they have if connected with a video already.
				var senders = getSenders2(UUID);
				senders.forEach(sender => {
					// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
					if (sender.track && sender.track.kind == "video") {
						sender.track.enabled = false; // I'm not entirely sure if I shoudl be doing this to a video stream... but I suppose new connections won't get a stream, and old connections will just replace it?
						getById("mutevideobutton").classList.add("hidden"); // hide the mute button, so they can't unmute while no video.
						//session.pcs[UUID].removeTrack(sender);  // replace may not be supported by all browsers.  eek.
						//errorlog("DELETED SENDER");
					}
				});
			
			}

			var msg = {};
			msg.videoMuted = true; // doesn;t matter if video is actually muted or not; no video is being sent
			session.sendMessage(msg);
		}
		return;
	} else {
		if (videoSelect && videoSelect.value) {
			SelectedVideoInputDevices = [videoSelect.value];
			saveSettings();
		}

		if (session.avatar && session.avatar.timer) {
			clearInterval(session.avatar.timer);
			session.avatar.timer = null;
		}

		var sq = 0;
		if (session.quality === false) {
			sq = session.roomid ? session.quality_room : session.quality_wb;
		} else if (session.quality > 2) {
			// 1080, 720, and 360p
			sq = 2; // hacking my own code. TODO: ugly, so I need to revisit this.
		} else {
			sq = session.quality;
		}

		if (session.director && quality !== false) {
			// URL-based quality won't matter if DIRECTOR;
			// quality = quality;
		} else if (quality === false || quality < sq) {
			quality = sq; // override the user's setting
		}

		if ((iOS || iPad) && SafariVersion < 15) {
			// iOS will not work correctly at 1080p; likely a h264 codec issue.
			if (quality == 0) {
				quality = 1;
			}
		}

		var constraints = {
			audio: false,
			video: getUserMediaVideoParams(quality, iOS || iPad)
		};

		//if (Firefox){
		//	constraints.video.height = constraints.video.height.ideal;
		//	constraints.video.width = constraints.video.height.ideal;
		//}

		log("Quality selected:" + quality);
		
		if (session.outboundVideoBitrate_userSet === false) {
			// default is 2500
			if (session.quality == 0) { // 1080p
				session.outboundVideoBitrate = 4000;
			} else if (session.quality == -1) { // unlocked
				session.outboundVideoBitrate = 4000;
			} else if (session.quality == -2) { // 4k
				session.outboundVideoBitrate = 8000;
			} else if (session.quality == -3) { // 2k
				session.outboundVideoBitrate = 6000;
			} else {
				session.outboundVideoBitrate = false;
			}
		}

		if (session.facingMode) {
			constraints.video.facingMode = { exact: session.facingMode }; // user or environment
		} else if (iOS || iPad) {
			constraints.video.deviceId = {
				exact: videoSelect.value
			}; // iPhone 6s compatible ? Needs to be exact for iPhone 6s
		} else if (Firefox) {
			// is firefox.
			constraints.video.deviceId = {
				exact: videoSelect.value
			}; // Firefox is a dick. Needs it to be exact.
		} else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) {
			// NDI does not like "EXACT"
			constraints.video.deviceId = videoSelect.value; // NDI is fucked up
		} else {
			constraints.video.deviceId = {
				exact: videoSelect.value
			}; //  Default. Should work for Logitech, etc.
		}

		if (session.width) {
			constraints.video.width = {
				exact: session.width
			}; // manually specified - so must be exact
		}
		if (session.height) {
			constraints.video.height = {
				exact: session.height
			};
		}

		if (session.frameRate) {
			constraints.video.frameRate = {
				exact: session.frameRate
			};
		} else if (session.maxframeRate != false) {
			constraints.video.frameRate = {
				ideal: session.maxframeRate,
				max: session.maxframeRate
			};
		} else if ((iOS || iPad) && SafariVersion > 15) {
			// iOS supports 720p60, but just 1080p30 : iphone 11 on march 2023
			if (quality === 1) {
				// iphone 11 and older
				if (!constraints.video.frameRate) {
					constraints.video.frameRate = {
						ideal: 60,
						max: 60
					};
				}
			} else if (iPhone12Up && quality < 1) {
				// iphone 12 and up?
				if (!constraints.video.frameRate) {
					try {
						if (videoSelect.options[videoSelect.selectedIndex].innerText.startsWith("Back ")) {
							// front seems to be limited to 720p60 / 1080p30
							constraints.video.frameRate = {
								ideal: 60,
								max: 60
							};
						}
					} catch (e) {
						errorlog(e);
					}
				}
			}
		}

		if (session.ptz) {
			if (constraints.video && constraints.video !== true) {
				if (ChromiumVersion && ChromiumVersion > 80) {
					constraints.video.pan = true;
					constraints.video.tilt = true;
					constraints.video.zoom = true;
				}
			}
		}

		if (session.forceAspectRatio) {
			// await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
			if (constraints.video && constraints.video !== true) {
				constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) };

				if (constraints.video.width && !session.width) {
					delete constraints.video.width;
				} else if (constraints.video.height && !session.height) {
					delete constraints.video.height;
				}
			}
		}

		var obscam = false;
		var mirrorcheck = false;
		log(videoSelect.options[videoSelect.selectedIndex].text);

		if (!videoSelect.options[videoSelect.selectedIndex]) {
			if (session.mobile) {
				mirrorcheck = true;
				mirror = false;
			} else {
				mirror = false;
			}
		} else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) {
			// OBS Virtualcam
			mirror = true;
			obscam = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) {
			// OBS Virtualcam
			mirror = true;
			obscam = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Streamlabs ")) {
			// OBS Virtualcam
			mirror = true;
			obscam = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Dummy video device")) {
			// Linuxv
			mirror = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("vMix Video")) {
			// vMix
			mirror = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Blackmagic")) {
			// Blackmagic devices
			mirror = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("screen-capture-recorder")) {
			// screen-capture-recorder
			mirror = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" back")) {
			// Android
			mirror = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" rear")) {
			// Android
			mirror = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) {
			// NDI Virtualcam
			mirror = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Back Camera")) {
			// iPhone and iOS
			mirror = true;
		} else if (videoSelect.options[videoSelect.selectedIndex].text.toLowerCase().includes("c922")) {
			if (session.quality !== 2 && !session.cleanOutput) {
				//getById("cameraTipContext1").innerHTML = getTranslation("camera-tip-c922");
				miniTranslate(getById("cameraTipContext1"), "camera-tip-c922");
				getById("cameraTip1").classList.remove("hidden");
			}
		} else if (videoSelect.options[videoSelect.selectedIndex].text.toLowerCase().includes("cam link")) {
			if (!session.cleanOutput) {
				//getById("cameraTipContext1").innerHTML = getTranslation("camera-tip-camlink");
				miniTranslate(getById("cameraTipContext1"), "camera-tip-camlink");
				getById("cameraTip1").classList.remove("hidden");
			}
		} else if (session.mobile) {
			mirrorcheck = true;
			mirror = false;
		} else {
			mirror = false;
		}

		if (SamsungASeries && ChromiumVersion) {
			if (!session.cleanOutput) {
				//getById("cameraTipContext1").innerHTML = getTranslation("samsung-a-series");
				miniTranslate(getById("cameraTipContext1"), "samsung-a-series");
				getById("cameraTip1").classList.remove("hidden");
			}
		}
		if (session.nomirror) {
			// do not have the camera be mirrored by default, unless using &mirror
			session.mirrorExclude = true;
		} else {
			session.mirrorExclude = mirror;
		}

		if (constraints.video && constraints.video !== true && Object.keys(constraints.video).length == 0) {
			constraints.video = true;
		} else if (constraints.video && constraints.video !== true && Object.keys(constraints.video).length == 1 && "deviceId" in constraints.video && "exact" in constraints.video.deviceId && constraints.video.deviceId.exact === "default") {
			constraints.video = true; // solves issues with IOS, where no permission yet given - can't request device ID it seems until permissions is given.
		}

		log(constraints);
		clearTimeout(grabVideoUserMediaTimeout);
		getUserMediaRequestID += 1;
		var gumMediaID = getUserMediaRequestID;
		var delayStart = 100;
		if (ChromiumVersion > 110) {
			// aded july 16th; speed up camera switching.
			delayStart = 20;
		} else if (Firefox) {
			delayStart = 500; // cause firefox is buggy as crap
		}
		grabVideoUserMediaTimeout = setTimeout(
			function (gumID, callback2) {
				if (getUserMediaRequestID !== gumID) {
					return;
				} // cancel
				
				if (Firefox){
					constraints = toFirefoxConstraint(constraints);
				}
				
				warnlog("navigator.mediaDevices.getUserMedia starting...");
				navigator.mediaDevices
					.getUserMedia(constraints)
					.then(function (stream) {
						if (getUserMediaRequestID !== gumID) {
							warnlog("GET USER MEDIA CALL HAS EXPIRED");
							stream.getTracks().forEach(function (track) {
								stream.removeTrack(track);
								track.stop();
								log("stopping old track");
							});
							return;
						}
						log("adding video tracks 2412");

						stream.getVideoTracks().forEach(async function (track) {
							try {
								if (mirrorcheck) {
									try {
										var capabilities = track.getCapabilities();
									} catch (e) {
										var capabilities = {};
									}
									if ("facingMode" in capabilities) {
										if (capabilities.facingMode == "environment") {
											session.mirrorExclude = true;
										}
									}
									if ("backgroundBlur" in capabilities) {
										// Chrome original trial, until v117, and then???
										query('#effectSelector option[value="13"]').classList.remove("hidden");
										query('#effectSelector option[value="13"]').disabled = null;
										query('#effectSelector3 option[value="13"]').classList.remove("hidden");
										query('#effectSelector3 option[value="13"]').disabled = null;
									} else {
										query('#effectSelector option[value="13"]').disabled = true;
										query('#effectSelector3 option[value="13"]').disabled = true;
									}
								}
							} catch (e) {}

							session.streamSrc.addTrack(track); // tracks previously removed.

							try {
								track.onended = function (e) {
									// hurrah!
									warnlog(e);
									refreshVideoDevice();
								};
							} catch (e) {
								errorlog(e);
							}

							if (session.whiteBalance !== false) {
								try {
									await track.applyConstraints({ advanced: [{ whiteBalanceMode: "manual", colorTemperature: parseInt(session.whiteBalance) }] });
								} catch (e) {
									errorlog(e);
									try {
										await track.applyConstraints({ advanced: [{ whiteBalanceMode: "manual" }] });
									} catch (e) {
										warnlog(e);
									}
								}
							}
							if (session.exposure !== false) {
								try {
									await track.applyConstraints({ advanced: [{ exposureMode: "manual", exposureTime: parseInt(session.exposure) }] });
								} catch (e) {
									errorlog(e);
									try {
										await track.applyConstraints({ advanced: [{ exposureMode: "manual" }] });
									} catch (e) {
										warnlog(e);
									}
								}
							}
							if (session.zoom !== false) {
								try {
									await track.applyConstraints({ advanced: [{ zoom: parseFloat(session.zoom) }] });
								} catch (e) {
									errorlog(e);
								}
							}
							if (session.saturation !== false) {
								try {
									await track.applyConstraints({ advanced: [{ saturation: parseInt(session.saturation) }] });
								} catch (e) {
									errorlog(e);
								}
							}
							if (session.sharpness !== false) {
								try {
									await track.applyConstraints({ advanced: [{ sharpness: parseInt(session.sharpness) }] });
								} catch (e) {
									errorlog(e);
								}
							}
							if (session.contrast !== false) {
								try {
									await track.applyConstraints({ advanced: [{ contrast: parseInt(session.contrast) }] });
								} catch (e) {
									errorlog(e);
								}
							}
							if (session.brightness !== false) {
								try {
									await track.applyConstraints({ advanced: [{ brightness: parseInt(session.brightness) }] });
								} catch (e) {
									errorlog(e);
								}
							}
							if (session.focusDistance !== false) {
								try {
									await track.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: parseInt(session.focusDistance) }] });
								} catch (e) {
									errorlog(e);
									try {
										await track.applyConstraints({ advanced: [{ focusMode: "manual" }] });
									} catch (e) {
										warnlog(e);
									}
								}
							}

							if (session.mobile) {
								if (!(iPad || iOS || Firefox)) {
									try {
										applySavedVideoSettings(track);
									} catch (e) {
										errorlog(e);
									}
								}
							}
						});

						if (Firefox && !FirefoxEnumerated) {
							if (session.streamSrc && session.streamSrc.getTracks().length) {
								FirefoxEnumerated = true;
								enumerateDevices().then(gotDevices);
							}
						}

						updateRenderOutpipe();
						// senderAudioUpdate

						if (wasDisabled && !session.videoMuted) {
							var msg = {};
							msg.videoMuted = session.videoMuted;
							session.sendMessage(msg);
						}

						applyMirror(session.mirrorExclude);

						session.videoElement.play().then(() => {
							log("play doublecheck completed");
						});

						if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) {
							if (session.autostart) {
								publishWebcam();
							} else {
								log("4620");
								if (document.getElementById("gear_webcam")) {
									updateStats(obscam);
								}
								if (document.getElementById("gowebcam")) {
									document.getElementById("gowebcam").dataset.ready = "true";
									if (document.getElementById("gowebcam").dataset.audioready == "true") {
										document.getElementById("gowebcam").disabled = false;
										//document.getElementById("gowebcam").innerHTML = getTranslation("start");
										miniTranslate(document.getElementById("gowebcam"), "start");
										document.getElementById("gowebcam").focus();
									}
								}
							}
						} else if (getById("gear_webcam3").style.display === "inline-block") {
							updateStats(obscam);
						}

						// Once crbug.com/711524 is fixed, we won't need to wait anymore. This is
						// currently needed because capabilities can only be retrieved after the
						// device starts streaming. This happens after and asynchronously w.r.t.
						// getUserMedia() returns.
						if (grabVideoTimer) {
							clearTimeout(grabVideoTimer);
							if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) {
								session.videoElement.controls = true;
							}
						}
						if (getById("popupSelector_constraints_video")) {
							getById("popupSelector_constraints_video").innerHTML = "";
						}
						if (getById("popupSelector_constraints_audio")) {
							getById("popupSelector_constraints_audio").innerHTML = "";
						}
						if (getById("popupSelector_constraints_loading")) {
							getById("popupSelector_constraints_loading").style.display = "";
						}

						if (iOS || iPad) {
							// TEMPORARY: iOS 15.3 beta fix
							toggleSpeakerMute(true);
						}
						if (!(eleName == "previewWebcam" || document.getElementById("previewWebcam"))) {
							updateMixer(); // not with the preview, but after.
						}

						pokeIframeAPI("local-camera-event");

						let grabVideoPostTimeoutValue = 1000;

						if (Firefox || session.mobile) {
							// wait longer for these; they are more likely to crash if too quick.
							grabVideoPostTimeoutValue = 2000;
						}

						grabVideoTimer = setTimeout(
							async function (callback3, gumid) {
								if (getUserMediaRequestID !== gumid) {
									// new camera selected in this time.
									return;
								}

								makeImages(true);

								if (getById("popupSelector_constraints_loading")) {
									getById("popupSelector_constraints_loading").style.display = "none";
								}
								if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) {
									session.videoElement.controls = true;
									try {
										var track0 = session.streamSrc.getVideoTracks();
										if (track0.length) {
											track0 = track0[0];
											if (track0.getCapabilities) {
												session.cameraConstraints = track0.getCapabilities();
											} else {
												session.cameraConstraints = {};
											}
											log(session.cameraConstraints);
											if (track0.getSettings) {
												session.currentCameraConstraints = track0.getSettings();

												if (screen && screen.orientation && screen.orientation.type) {
													if (screen.orientation.type.includes("portrait")) {
														if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
															session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
														}
													}
												} else if (window.matchMedia("(orientation: portrait)").matches) {
													if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
														session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
													}
												}
											} else {
												session.currentCameraConstraints = {};
											}
											log(session.currentCameraConstraints);
										}
									} catch (e) {
										errorlog(e);
									}
								} else if (toggleSettingsState) {
									log("16047");
									updateConstraintSliders(); //listCameraSettings();
								}
								if (callback3) {
									try {
										var data = {};
										data.UUID = callback3;
										data.videoOptions = listVideoSettingsPrep();
										sendMediaDevices(data.UUID);
										session.sendMessage(data, data.UUID);
									} catch (e) {}
								}

								if (iOS || iPad) {
									// TEMPORARY: iOS 15.3 beta fix
									toggleSpeakerMute(true);
								}

								if (session.forceAspectRatio) {
									await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
								}

								updateForceRotate(); // this contains session.setResolution();

								if (iOS || iPad) {
									// if we don't do this, portrait videos may be detected as horizontal
									if (!document.getElementById("previewWebcam")) {
										updateMixer(); // not with the preview, but after.
									}
								}

								try {
									if (session.pip3) {
										if (!eleName.pip) {
											eleName.pip = true;
											toggleSystemPip(session.videoElement, true);
										}
									}
								} catch (e) {}

								// this will reset scaling for all viewers of this stream. I also call it when aspect ratio, width, or height is changed via applyConstraints

								dragElement(session.videoElement);
							},
							grabVideoPostTimeoutValue,
							callback2,
							gumID
						); // focus

						log("DONE - found stream");
					})
					.catch(function (e) {
						if (getUserMediaRequestID !== gumID) {
							warnlog("the previously selected camera attempted failed, but not a big deal, since its now void");
							return;
						}

						warnlog(e);
						if (e.name === "OverconstrainedError") {
							warnlog(e.message || e);
							log("Resolution or frameRate didn't work");
						} else if (e.name === "NotReadableError") {
							if (quality <= 10) {
								activatedPreview = false;
								grabVideo(quality + 1, eleName, selector);
							} else if (session.facingMode) {
								session.facingMode = false;
								activatedPreview = false;
								grabVideo(false, eleName, selector); // restart.
							} else {
								if (!session.cleanOutput) {
									if (iOS) {
										warnUser("An error occured. Closing existing tabs in Safari may solve this issue.");
									} else {
										warnUser("Error: Could not start video source.\n\nTypically this means the Camera is already be in use elsewhere. Most webcams can only be accessed by one program at a time.\n\nTry a different camera or perhaps try re-plugging in the device.");
									}
								}
								activatedPreview = true;
								if (getById("gowebcam")) {
									getById("gowebcam").innerHTML = "Problem with Camera";
								}
							}
							return;
						} else if (e.name === "NavigatorUserMediaError") {
							if (getById("gowebcam")) {
								getById("gowebcam").innerHTML = "Problem with Camera";
							}
							if (!session.cleanOutput) {
								warnUser("Unknown error: 'NavigatorUserMediaError'");
							}
							return;
						} else if (e.name === "timedOut") {
							activatedPreview = true;
							if (getById("gowebcam")) {
								getById("gowebcam").innerHTML = "Problem with Camera";
							}
							if (!session.cleanOutput) {
								warnUser(e.message);
							}
							return;
						} else {
							errorlog("An unknown camera error occured");
						}

						if (quality <= 10) {
							activatedPreview = false;
							grabVideo(quality + 1, eleName, selector);
						} else if (session.facingMode) {
							session.facingMode = false;
							activatedPreview = false;
							grabVideo(false, eleName, selector); // restart.
						} else {
							errorlog("********Camera failed to work");
							activatedPreview = true;
							if (getById("gowebcam")) {
								getById("gowebcam").innerHTML = "Problem with Camera";
							}
							if (!session.cleanOutput) {
								if (session.width || session.height || session.frameRate) {
									warnUser("<i class='las la-exclamation-circle'></i> Camera failed to load.\n\nPlease ensure your camera supports the resolution and frameRate that has been manually specified. Perhaps use &quality=0 instead.", false, false);
								} else {
									warnUser("<i class='las la-exclamation-circle'></i> Camera failed to load.\n\nPlease make sure it is not already in use by another application.\n\nPlease make sure you have accepted the camera permissions.", false, false);
								}
							}
						}
					});
			},
			delayStart,
			gumMediaID,
			callback
		);
	}
}

function updateRenderOutpipe() {
	// video only.
	log("updateRenderOutpipe()");

	if (session.canvasWebGL) {
		session.canvasWebGL.remove();
		session.canvasWebGL = null;
	}

	if (session.canvasSource) {
		session.canvasSource.srcObject.getTracks().forEach(function (trk) {
			session.canvasSource.srcObject.removeTrack(trk);
			//trk.stop();
		});
	}

	if (session.videoElement && session.videoElement.srcObject) {
		session.videoElement.srcObject.getVideoTracks().forEach(function (track) {
			session.videoElement.srcObject.removeTrack(track);
			//track.stop();
			//session.videoElement.load();
		});
	} else {
		checkBasicStreamsExist();
	}

	if (session.streamSrc) {
		var tracks = session.streamSrc.getVideoTracks();

		if (!tracks.length || session.videoMuted) {
			tracks = setAvatarImage(tracks);
			if (tracks.length) {
				if (tracks.length && !session.cleanOutput && !session.cleanish) {
					getById("mutevideobutton").classList.remove("hidden");
				}

				tracks.forEach(function (track) {
					session.videoElement.srcObject.addTrack(track);
					if (session.avatar && session.avatar.tracks) {
						var msg = {};
						msg.videoMuted = false; // doesn't matter actual mute state, since its the avatar
						session.sendMessage(msg);
					} else {
						toggleVideoMute(true);
					}
					pushOutVideoTrack(track); // video only
				});
			} else {
				var msg = {};
				msg.videoMuted = true;
				session.sendMessage(msg);
				session.videoElement.load();
				getById("mutevideobutton").classList.add("hidden");
			}
		} else if (tracks.length) {
			applyMirror(session.mirrorExclude);
			tracks.forEach(function (track) {
				track = applyEffects(track); // updates with the correct track session.streamSrc
				session.videoElement.srcObject.addTrack(track);
				toggleVideoMute(true);
				pushOutVideoTrack(track); // video only
			});

			if (tracks.length && !session.cleanOutput && !session.cleanish) {
				getById("mutevideobutton").classList.remove("hidden");
			}
		}
	}
}

function pushOutVideoTrack(track) {
	log("pushOutVideoTrack");

	pokeIframeAPI("push-video-track", track.id, false, session.streamID); // (action, value = null, UUID = null, SID=null)

	
	if (session.chunked) {
		for (UUID in session.pcs) {
			session.chunkedStream(UUID); // I need to update chunkedStream with the current track? If sstype=3, then skip this
		}
	}

	if (session.audioContentHint && track.kind === "audio") {
		// why am I pushing an audio track?
		errorlog("this shouldn't occur, since only video tracks are expected");
		try {
			track.contentHint = session.audioContentHint;
		} catch (e) {
			errorlog(e);
		}
	}
	if (session.screenShareState && session.screenshareContentHint && track.kind === "video") {
		// I need to check if this is actually a screenshare before setting the hint (sstype=3)
		try {
			track.contentHint = session.screenshareContentHint;
		} catch (e) {
			errorlog(e);
		}
	} else if (session.contentHint && track.kind === "video") {
		try {
			track.contentHint = session.contentHint;
		} catch (e) {
			errorlog(e);
		}
	}

	if (session.whipOut && session.whipOut.getSenders) {
		// should only be 0 or 1 video sender, ever.
		//var added = false;
		session.whipOut.getSenders().forEach(sender => {
			// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
			if (sender.track && sender.track.kind == "video") {
				warnlog("Replacing track");
				sender.replaceTrack(track); // replace may not be supported by all browsers.  eek.
				//sender.track.enabled = true;
				//added = true;
			}
		});
	}

	for (UUID in session.pcs) {
		var videoAdded = false;
		try {
			if ("realUUID" in session.pcs[UUID]) {
				continue;
			}
			if (session.chunked && session.pcs[UUID].allowChunked){
				continue;
			}
			
			if (session.pcs[UUID].guest == true && session.roombitrate === 0) {
				log("room rate restriction detected. No videos will be published to other guests");
			} else if (session.pcs[UUID].allowVideo == true) {
				// allow

				// for any connected peer, update the video they have if connected with a video already.
				var added = false;
				var senders = getSenders2(UUID);
				senders.forEach(sender => {
					// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
					if (added) {
						return;
					}
					if (sender.track && sender.track.kind == "video") {
						sender.replaceTrack(track); // replace may not be supported by all browsers.  eek.
						log("Track replaced");
						log(track);
						sender.track.enabled = true;
						added = true;
					}
				});
				if (added == false) {
					videoAdded = true;
					session.pcs[UUID].addTrack(track, session.videoElement.srcObject); // can't replace, so adding
					setTimeout(
						function (uuid) {
							session.optimizeBitrate(uuid);
						},
						session.rampUpTime,
						UUID
					); // 3 seconds lets us ramp up the quality a bit and figure out the total bandwidth quicker
				}
			}
		} catch (e) {
			errorlog(e);
		}

		if (iOS || iPad) {
			///////// THIS IS A FIX FOR iOS 15.4.  When a video is loaded (view/push), the bitrate from iOS devices is stuck low, and resolution needs toggle to fix.
			// videoAdded value needs to be deleted from above also
			if (SafariVersion && SafariVersion <= 13) {
				//
			} else if (videoAdded) {
				setTimeout(
					function (uuid) {
						session.setScale(uuid, null);
					},
					2000,
					UUID
				);
				setTimeout(
					function (uuid) {
						var scale = 100;
						session.setScale;
						if (session.pcs[uuid].scale) {
							scale = session.pcs[uuid].scale;
						}
						session.setScale(uuid, scale);
					},
					5000,
					UUID
				);
			}
		}
	}
	if (track.kind === "audio") {
		session.applyIsolatedChat();
	}
	session.refreshScale();
}

async function grabAudio(selector = "#audioSource", trackid = null, override = false, callbackUUID = false, callback = false) {
	// trackid is the excluded track , callback is UUID

	if (activatedPreview == true) {
		log("activated preview return 2");
		return;
	}
	activatedPreview = true;
	log("TRACK EXCLUDED:" + trackid);

	try {
		var baseTest = document.querySelector(selector);
		if (!baseTest) {
			errorlog("No audio source menu");
			return;
		}
		if (baseTest && baseTest.tagName == "UL") {
			var audioSelect = baseTest.querySelectorAll("input");
			var audioExcludeList = [];
			for (var i = 0; i < audioSelect.length; i++) {
				try {
					if ("screen" == audioSelect[i].dataset.type) {
						// skip already excluded ---------- !!!!!!  DOES THIS MAKE SENSE? TODO: CHECK
						if (audioSelect[i].checked) {
							audioExcludeList.push(audioSelect[i]);
						}
					}
				} catch (e) {
					errorlog(e);
				}
			}
		} else if (baseTest && baseTest.tagName == "SELECT") {
			var audioExcludeList = [];
			var audioSelect = baseTest.options;
			for (var i = 0; i < audioSelect.length; i++) {
				try {
					if ("screen" == audioSelect[i].dataset.type) {
						// skip already excluded ---------- !!!!!!  DOES THIS MAKE SENSE? TODO: CHECK
						if (audioSelect[i].selected) {
							audioExcludeList.push(audioSelect[i]);
						}
					}
				} catch (e) {
					errorlog(e);
				}
			}
		}
	} catch (e) {
		errorlog(e);
	}

	try {
		if (session.videoElement && session.videoElement.srcObject) {
			session.videoElement.srcObject.getAudioTracks().forEach(function (track) {
				// TODO: Confirm that I even need this?
				for (var i = 0; i < audioExcludeList.length; i++) {
					try {
						if (audioExcludeList[i].label == track.label) {
							warnlog("DONE");
							return;
						}
					} catch (e) {
						errorlog(e);
					}
				}
				if (trackid && track.id == trackid) {
					warnlog("SKIPPED EXCLUDED TRACK?");
					return;
				}
				session.videoElement.srcObject.removeTrack(track);
				track.stop(); // remove then stop.
			});
		} else {
			// if no stream exists
			checkBasicStreamsExist();
		}
	} catch (e) {
		errorlog(e);
	}

	try {
		if (session.streamSrc) {
			session.streamSrc.getAudioTracks().forEach(function (track) {
				for (var i = 0; i < audioExcludeList.length; i++) {
					try {
						if (audioExcludeList[i].label == track.label) {
							warnlog("EXCLUDING TRACK; PROBABLY SCREEN SHARE");
							return;
						}
					} catch (e) {
						errorlog(e);
					}
				}
				if (trackid && track.id == trackid) {
					warnlog("SKIPPED EXCLUDED TRACK?");
					return;
				}
				session.streamSrc.removeTrack(track);
				track.stop();
			});
		} else {
			// if no stream exists
			checkBasicStreamsExist();
		}
	} catch (e) {
		errorlog(e);
	}

	try {
		if (session.streamSrcClone) {
			session.streamSrcClone.getAudioTracks().forEach(function (track) {
				for (var i = 0; i < audioExcludeList.length; i++) {
					try {
						if (audioExcludeList[i].label == track.label) {
							warnlog("EXCLUDING TRACK; PROBABLY SCREEN SHARE");
							return;
						}
					} catch (e) {
						errorlog(e);
					}
				}
				if (trackid && track.id == trackid) {
					warnlog("SKIPPED EXCLUDED TRACK?");
					return;
				}
				session.streamSrcClone.removeTrack(track);
				track.stop();
			});
		}
	} catch (e) {
		errorlog(e);
	}

	var streams = await getAudioOnly(selector, trackid, override); // Get audio streams

    try {
        log("STREAMS: " + streams.length);

        for (var i = 0; i < streams.length; i++) {
            streams[i].getAudioTracks().forEach(function (track) {
                try {
                    session.streamSrc.addTrack(track); // add video track to the preview video

                    track.onended = handleAudioTrackEnded; // Add event listener for track end
                    
                    log("ok?");
                    // applySavedAudioSettings(track); ## this doesn't work as echo-cancellation(+) needs to be applied via getuserMedia only.
                } catch (e) {
                    errorlog(e);
                }
            });
        }
    } catch (e) {
        errorlog(e);
    }
	if (Firefox && !FirefoxEnumerated) {
		if (session.streamSrc && session.streamSrc.getTracks().length) {
			FirefoxEnumerated = true;
			enumerateDevices().then(gotDevices);
		}
	}

	if (callback) {
		callback();
	}

	senderAudioUpdate(callbackUUID); 
}

session.toggleSoloChat = function (UUID, event = false) {
	// ==> applyIsolatedChat -- this should be trigger by the director only I think

	if (session.director) {
		if (!session.directorEnabledPPT) {
			warnUser("Enable the director's microphone first.", 2000);
			return false;
		}
	}

	if (Firefox) {
		warnlog("Solo talk support for Firefox is currently experimental");
	}

	var msg = {};
	msg.micIsolate = false;

	if (session.soloChatUUID.includes(UUID)) {
		// already added, so lets toggle off
		session.soloChatUUID.splice(session.soloChatUUID.indexOf(UUID), 1); // Toggles.  Adds target to soloChatUUID list
		msg.lowerVolume = false;
	} else {
		session.soloChatUUID.push(UUID); //not added, so lets toggle on
		msg.lowerVolume = true;
	}

	if (event) {
		if (event.ctrlKey || event.metaKey) {
			if (session.soloChatUUID.includes(UUID)) {
				msg.micIsolate = 1;
			}
		}
	}

	session.sendRequest(msg, UUID);

	log(session.soloChatUUID);

	var ele = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + UUID + '"]'); // [data--u-u-i-d="'+UUID+'"]  // this all just updates the buttons
	log(ele);

	var ret = 0;

	if (session.soloChatUUID.includes(UUID)) {
		if (msg.micIsolate) {
			ret = 2;
			ele.classList.add("altpress"); // we will do this later.
			ele.value = 2;
		} else {
			ret = 1;
			ele.value = 1;
		}
	} else {
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		ele.classList.remove("altpress");
		ele.value = 0;
	}

	session.applySoloChat(false);
	return ret;
};
///////////////////////

session.togglePrivateChat = function (ele) {
	var msg = {};
	warnlog(ele);
	if (ele.value == 0) {
		msg.micIsolate = true;
		ele.value = 1;
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
	} else {
		msg.micIsolate = false;
		ele.value = 0;
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
	}
	session.sendRequest(msg, ele.dataset.UUID);
	warnlog(msg);
};

// we call this via session.applyIsolatedChat, just in case
session.applyIsolatedVolume = function () {
	//  mutes outbound mic audio; for guests, and not the director

	var i = session.lowerVolume.length;
	while (i--) {
		if (!(session.lowerVolume[i] in session.rpcs)) {
			// clean up dead connections
			session.lowerVolume.splice(i, 1);
		}
	}

	var soloMode = false;

	/* if (!(session.cleanOutput)){
		if (session.lowerVolume.length){
			getById("header").classList.add('orange');
			getById("head6").classList.remove('hidden');
		} else if (session.audioGain === 0){
			// do nothing.
		} else {
			getById("header").classList.remove('orange');
			getById("head6").classList.add('hidden');
		}
	} */

	if (session.lowerVolume.length) {
		soloMode = true;
	}

	if (soloMode) {
		for (var UUID in session.rpcs) {
			if (session.lowerVolume.includes(UUID)) {
				if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume !== false) {
					// isolated
					session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume;
					session.rpcs[UUID].savedVolume = false;
				}
				continue;
			}
			if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume == false) {
				// not isolated
				session.rpcs[UUID].savedVolume = session.rpcs[UUID].videoElement.volume;
				session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume * 0.25;
			}
		}
	} else {
		for (var UUID in session.rpcs) {
			if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume !== false) {
				// isolated
				session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume;
				session.rpcs[UUID].savedVolume = false;
			}
		}
	}
};

session.applyIsolatedChat = function (UUID = false) {
	//  mutes outbound mic audio; for guests, and not the director
	log("applyIsolatedChat");
	session.applyIsolatedVolume(); // this toggle the speaker output

	var i = session.micIsolated.length;
	while (i--) {
		if (!(session.micIsolated[i] in session.pcs) && !(session.micIsolated[i] in session.rpcs)) {
			session.micIsolated.splice(i, 1);
		}
	}

	var muteList = [...session.micIsolated]; // one thing I hate about Javascript. Doesn't actually copy arrays.
	var soloMode = false;

	if (session.micIsolatedAutoMute) {
		// session.micIsolatedAutoMute
		soloMode = true;
		session.micIsolatedAutoMute.forEach(uid => {
			if (!muteList.includes(uid) && (uid in session.rpcs || uid in session.pcs)) {
				muteList.push(uid);
			}
		});
	}

	if (muteList.length) {
		soloMode = true;
	}

	if (!session.cleanOutput) {
		if (soloMode) {
			getById("header").classList.add("orange");
			getById("head6").classList.remove("hidden");
		} else if (session.audioGain === 0) {
			// do nothing.
		} else {
			getById("header").classList.remove("orange");
			getById("head6").classList.add("hidden");
		}
	}

	/////
	if (session.directorSpeakerMuted !== null) {
		for (var uuid in session.rpcs) {
			try {
				var receivers = getReceivers2(uuid); //session.rpcs[uuid].getReceivers();
				for (var i = 0; i < receivers.length; i++) {
					if (receivers[i].track.kind == "audio") {
						receivers[i].track.enabled = !session.directorSpeakerMuted;
					}
				}
			} catch (e) {
				errorlog(e);
			}
		}
		if (session.directorSpeakerMuted) {
			getById("videosource").muted = true;
		}
	}
	//////////////

	if (UUID) {
		try {
			var senders = getSenders2(UUID);
			senders.forEach(sender => {
				if (!sender.track) {
					return;
				}
				if (sender.track.kind !== "audio") {
					return;
				}

				var settings = {};
				if (!soloMode) {
					settings.active = true;
					session.pcs[UUID].audioMutedOverride = false;
				} else if (muteList.indexOf(UUID) >= 0) {
					settings.active = true;
					session.pcs[UUID].audioMutedOverride = false;
				} else {
					log("MUTING via session.applyIsolatedChat");
					settings.active = false;
					session.pcs[UUID].audioMutedOverride = true;
				}
				setEncodings(sender, settings);
			});
		} catch (e) {
			errorlog(e);
		}
	} else {
		for (var UUID in session.pcs) {
			try {
				var senders = getSenders2(UUID);
				senders.forEach(sender => {
					if (!sender.track) {
						return;
					}
					if (sender.track.kind !== "audio") {
						return;
					}

					var settings = {};
					if (!soloMode) {
						settings.active = true;
						session.pcs[UUID].audioMutedOverride = false;
					} else if (muteList.indexOf(UUID) >= 0) {
						settings.active = true;
						session.pcs[UUID].audioMutedOverride = false;
					} else {
						log("MUTING via session.applyIsolatedChat");
						settings.active = false;
						session.pcs[UUID].audioMutedOverride = true;
					}
					setEncodings(sender, settings);
				});
			} catch (e) {
				errorlog(e);
			}
		}
	}
};

var FirefoxSenders = {};

function setEncodings(sender, settings = null, callback = null, cbarg = null) {
	if (!settings) {
		if (!sender.encodingsQueue) {
			// not set
			return;
		} else if (!sender.encodingsQueue.length) {
			// none left
			return;
		}
	} else if (!("encodingsQueue" in sender)) {
		sender.encodingsQueue = [[settings, callback, cbarg]];
	} else {
		sender.encodingsQueue.push([settings, callback, cbarg]);
	}

	if (sender.encodingsQueueActive) {
		return;
	}

	try {
		sender.encodingsQueueActive = true; // we're now busy.

		var options = sender.encodingsQueue.shift();
		settings = options[0];
		callback = options[1];
		cbarg = options[2];

		const params = sender.getParameters();
		if (!params.encodings || params.encodings.length == 0) {
			params.encodings = [{}];
		}
		var changed = false;
		for (var setting in settings) {
			if (settings[setting] === null) {
				if (setting in params.encodings[0]) {
					delete params.encodings[0][setting];
					changed = true;
				}
			} else {
				if (setting in params.encodings[0]) {
					if (params.encodings[0][setting] !== settings[setting]) {
						changed = true;
					}
				} else {
					changed = true;
				}
				params.encodings[0][setting] = settings[setting];
			}
		}

		log(settings);

		// if old Firefox, see if I can do something other than Active?

		if (!changed && !Firefox && !SafariVersion) {
			log("SET ENCODINGS MATCH INPUT; skipping");
			if (callback) {
				if (cbarg) {
					setTimeout(function () {
						callback(cbarg);
					}, 0);
				} else {
					setTimeout(function () {
						callback();
					}, 0);
				}
			}
			sender.encodingsQueueActive = false;
			setEncodings(sender);
			return;
		}

		if (Firefox && !(Firefox >= 110)) {
			// https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpEncodingParameters now supported in v110, but old versions will need this function still
			if ("active" in settings) {
				warnlog("Firefox does not support track active state. We will use enable/disable for that instead.");
				if (FirefoxSenders.sender) {
					if (FirefoxSenders.sender.lastState === false) {
						FirefoxSenders.sender.activeState = settings.active;
						// already set to false, so should stay disabled
					} else {
						FirefoxSenders.sender.activeState = settings.active;
						sender.track.enabled = settings.active; // either true or false
					}
				} else {
					FirefoxSenders.sender = { lastState: sender.track.enabled, activeState: settings.active };
					sender.track.enabled = settings.active;
				}

				delete settings.active;
				if (!Object.keys(settings).length) {
					if (callback) {
						if (cbarg) {
							setTimeout(function () {
								callback(cbarg);
							}, 0);
						} else {
							setTimeout(function () {
								callback();
							}, 0);
						}
					}
					log("COMPELTED FIREFOX SET ENCODINGS");
					sender.encodingsQueueActive = false;
					setEncodings(sender);
					return;
				}
			}
		} else if (Firefox) {
			// Firefox , all versions, don't support active state with audio yet?? GAhhhhhhhh!
			if ("track" in sender && "kind" in sender.track && sender.track.kind == "audio") {
				if ("active" in settings) {
					warnlog("Firefox does not support track active state with AUDIO yet... We will use enable/disable for that instead.");
					if (FirefoxSenders.sender) {
						if (FirefoxSenders.sender.lastState === false) {
							FirefoxSenders.sender.activeState = settings.active;
							// already set to false, so should stay disabled
						} else {
							FirefoxSenders.sender.activeState = settings.active;
							sender.track.enabled = settings.active; // either true or false
						}
					} else {
						FirefoxSenders.sender = { lastState: sender.track.enabled, activeState: settings.active };
						sender.track.enabled = settings.active;
					}

					delete settings.active;
					if (!Object.keys(settings).length) {
						if (callback) {
							if (cbarg) {
								setTimeout(function () {
									callback(cbarg);
								}, 0);
							} else {
								setTimeout(function () {
									callback();
								}, 0);
							}
						}
						log("COMPELTED FIREFOX SET ENCODINGS");
						sender.encodingsQueueActive = false;
						setEncodings(sender);
						return;
					}
				}
			}
		}

		sender
			.setParameters(params)
			.then(() => {
				if (callback) {
					if (cbarg) {
						setTimeout(function () {
							callback(cbarg);
						}, 0);
					} else {
						setTimeout(function () {
							callback();
						}, 0);
					}
				}
				sender.encodingsQueueActive = false;
				setEncodings(sender);
			})
			.catch(e => {
				errorlog(e);
				sender.encodingsQueueActive = false;
				setEncodings(sender);
			});
	} catch (e) {
		errorlog(e);
		sender.encodingsQueueActive = false;
	}
}

session.applySoloChat = function (apply = true) {
	// mutes outbound mic audio; ;;  does the actual solo chat muting for the director
	if (session.director === false) {
		session.applyIsolatedChat();
		return;
	} else if (!session.directorEnabledPPT) {
		return;
	}

	log("applySoloChat()");

	var i = session.soloChatUUID.length;
	while (i--) {
		if (!(session.soloChatUUID[i] in session.pcs)) {
			session.soloChatUUID.splice(i, 1);
			log("splicing out: " + i);
		}
	}

	for (var uuid in session.pcs) {
		// not sure what to do here wrt to screen tracks
		try {
			var senders = getSenders2(uuid);
			senders.forEach(sender => {
				if (!sender.track) {
					return;
				}
				if (sender.track.kind !== "audio") {
					return;
				}

				var settings = {};

				if (session.soloChatUUID.length && session.soloChatUUID.includes(uuid)) {
					settings.active = true;
					setEncodings(
						sender,
						settings,
						function (uid) {
							log("2: " + uid);
							var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]');
							if (button) {
								button.classList.add("pressed");
								button.ariaPressed = "true";
								button.classList.remove("hint");
							}
						},
						uuid
					);
				} else if (session.soloChatUUID.length == 0) {
					settings.active = true;
					setEncodings(
						sender,
						settings,
						function (uid) {
							log(uid);
							var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]');
							if (button) {
								button.classList.remove("pressed");
								button.ariaPressed = "false";
								button.classList.remove("hint");
							}
						},
						uuid
					);
				} else {
					settings.active = false;
					setEncodings(
						sender,
						settings,
						function (uid) {
							warnlog("muted the output to:" + uid);
							var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]');
							if (button) {
								button.classList.remove("pressed");
								button.ariaPressed = "false";
								button.classList.add("hint");
							}
						},
						uuid
					);
				}
			});
		} catch (e) {
			errorlog(e);
		}
	}
	if (apply == false) {
		if (session.soloChatUUID.length) {
			session.muted_savedState = session.muted;
			session.muted = false;
			data = {};
			data.muteState = session.muted;
			for (var i = 0; i < session.soloChatUUID.length; i++) {
				session.sendMessage(data, session.soloChatUUID[i]);
			}
		} else {
			session.muted = session.muted_savedState;
		}
		toggleMute(true);
	}
};

function senderAudioUpdate(callbackUUID = false, videoSource=null) {
	try {
		let tracks = [];
		
		if (!videoSource){
			checkBasicStreamsExist();
			videoSource =  session.videoElement.srcObject;
		} 
		
		tracks = videoSource.getAudioTracks();
		

		if (session.audioContentHint && tracks.length) {
			tracks.forEach(trk => {
				try {
					trk.contentHint = session.audioContentHint;
				} catch (e) {
					errorlog(e);
				}
			});
		}

		if (session.whipOut && session.whipOut.getSenders && tracks.length) {
			// mixMinus won't work with meshcast, so don't bother.
			session.whipOut.getSenders().forEach(sender => {
				// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
				if (sender.track && sender.track.kind == "audio") {
					tracks.forEach(trk => {
						sender.replaceTrack(trk);
					});
				}
			});
		}

		for (UUID in session.pcs) {
			if ("realUUID" in session.pcs[UUID]) {
				continue;
			} // do not process the screen share audio
			if (session.chunked && session.pcs[UUID].allowChunked && (session.pcs[UUID].allowChunked!==2)) {
				continue;
			}
			
			if (session.pcs[UUID].allowAudio == true) {
				var senders = getSenders2(UUID);
				if (session.mixMinus) {
					log("mixMinus START ..");
					var STRM = mixMinusAudio(UUID);
					if (!STRM) {
						continue;
					}
					STRM.getAudioTracks().forEach(trk => {
						if (session.audioContentHint) {
							trk.contentHint = session.audioContentHint;
						}

						var added = false;
						senders.forEach(sender => {
							if (added) {
								if (sender.track && sender.track.kind == "audio") {
									sender.track.enabled = false;
								}
								return;
							}
							if (sender.track && sender.track.kind == "audio") {
								sender.replaceTrack(trk);
								sender.track.enabled = true;
								added = true;
								warnlog("ADDED 5");
							}
						});
						if (added) {
							return;
						}
						session.pcs[UUID].addTrack(trk, STRM);
					});
					continue;
				}
				senders.forEach(sender => {
					var good = false;
					if (sender.track && sender.track.id && sender.track.kind == "audio") {
						tracks.forEach(function (track) {
							// audio also
							if (track.id == sender.track.id) {
								good = true;
							}
						});
					} else {
						// video or something else; ignore it.
						return;
					}
					if (good) {
						return;
					}
					sender.track.enabled = false;
					//session.pcs[UUID].removeTrack(sender); //  Apparently removeTrack causes renogiation; also kills send/recv.
				});

				if (tracks.length) {
					tracks.forEach(function (track) {
						var matched = false;
						var senders = getSenders2(UUID);
						senders.forEach(sender => {
							if (sender.track && sender.track.id && sender.track.kind == "audio") {
								warnlog(sender.track.id + " " + track.id);
								if (sender.track.id == track.id) {
									warnlog("MATCHED 1");
									matched = true;
								}
							}
						});
						if (matched) {
							return;
						}
						var added = false;
						var senders = getSenders2(UUID);
						senders.forEach(sender => {
							if (added) {
								return;
							}
							if (sender.track && sender.track.kind == "audio" && sender.track.enabled == false) {
								sender.replaceTrack(track);
								sender.track.enabled = true;
								added = true;
								warnlog("ADDED 2");
							}
						});
						if (added) {
							return;
						}
						var sender = session.pcs[UUID].addTrack(track, videoSource);
						
					});
				} else {
					var senders = getSenders2(UUID);
					senders.forEach(sender => {
						if (sender.track && sender.track.kind == "audio") {
							sender.track.enabled = false; // (trying this instead)
							//session.pcs[UUID].removeTrack(sender); //  Apparently removeTrack causes renogiation; also kills send/recv.
						}
					});
				}
			}
		}
		if (session.director !== false) {
			session.applySoloChat(); //  mute streams that should be muted if a director
		}
		session.applyIsolatedChat();

		try {
			if (toggleSettingsState) {
				updateConstraintSliders();
			}
		} catch (e) {}

		if (callbackUUID) {
			try {
				var data = {};
				data.UUID = callbackUUID;
				data.audioOptions = listAudioSettingsPrep();
				sendMediaDevices(data.UUID);
				session.sendMessage(data, data.UUID);
			} catch (e) {}
		}

		if (session.twilio) {
			session.twilio.updateMixer();
		}
	} catch (e) {
		errorlog(e);
	}

	if (document.getElementById("gowebcam")) {
		document.getElementById("gowebcam").dataset.audioready = true;
		if (document.getElementById("gowebcam").dataset.ready && document.getElementById("gowebcam").dataset.ready == "true") {
			document.getElementById("gowebcam").disabled = false;
			miniTranslate(document.getElementById("gowebcam"), "start");
			document.getElementById("gowebcam").focus();
		}
	}
}

async function press2talk(clean = false) {
	var ele = getById("press2talk");
	ele.style.minWidth = "127px";
	ele.style.padding = "7px";
	getById("settingsbutton").classList.remove("hidden");

	if (!document.getElementById("controls_director") && session.showDirector) {
		createDirectorOnlyBox();
	}

	if (session.taintedSession) {
		var msg = {};
		msg.virtualHangup = false;
		session.sendMessage(msg);
	}

	log("DIRECTOR STREAM SETUP");

	if (getById("press2talk").dataset.enabled == true) {
		log("already enabled");
		return;
	}
	getById("press2talk").dataset.enabled = true;
	getById("press2talk").outerHTML = "";
	getById("mutebutton").classList.remove("hidden");
	getById("hangupbutton2").classList.remove("hidden");

	if (!session.showDirector && session.recordLocal !== false) {
		getById("recordLocalbutton").classList.remove("hidden");
	}

	if (session.screenshareType === 3) {
		getById("screenshare3button").className = "float";
		getById("screensharebutton").className = "float hidden";
		getById("screenshare2button").className = "float hidden";
	} else if (session.screenshareType === 1) {
		getById("screensharebutton").className = "float";
		getById("screenshare3button").className = "float hidden";
		getById("screenshare2button").className = "float hidden";
	} else if (session.screenshareType === 2) {
		getById("screenshare2button").className = "float";
		getById("screensharebutton").className = "float hidden";
		getById("screenshare3button").className = "float hidden";
	} else if (session.broadcast === null) {
		// sstype=1, since in self-broadcast mode
		getById("screensharebutton").className = "float";
		getById("screenshare2button").className = "float hidden";
		getById("screenshare3button").className = "float hidden";
	} else {
		// sstype=3, since not in broadcast mode
		getById("screensharebutton").className = "float hidden";
		getById("screenshare2button").className = "float hidden";
		getById("screenshare3button").className = "float";
	}

	checkBasicStreamsExist();
	session.videoElement.id = "videosource"; // could be set to UUID in the future
	session.videoElement.dataset.menu = "context-menu-video";

	if (session.streamID) {
		session.videoElement.dataset.sid = session.streamID;
	}

	// videosource
	session.videoElement.muted = true;
	session.videoElement.autoplay = true;
	session.videoElement.controls = session.showControls || false;
	session.videoElement.setAttribute("playsinline", "");

	if (document.getElementById("videoContainer_director")) {
		getById("videoContainer_director").appendChild(session.videoElement);
	} else {
		getById("miniPerformer").appendChild(session.videoElement);
	}

	if (session.screenShareElement && document.getElementById("videoScreenContainer_director")) {
		getById("videoScreenContainer_director").appendChild(session.screenShareElement);
	} else if (session.screenShareElement) {
		getById("miniPerformer").appendChild(session.screenShareElement);
	}

	session.videoElement.title = "This is the preview of the Director's audio and video output.";

	session.videoElement.onpause = event => {
		// prevent things from pausing; human or other

		if (!(event.ctrlKey || event.metaKey)) {
			log("Video paused; auto playing");
			event.currentTarget
				.play()
				.then(_ => {
					log("playing 9");
				})
				.catch(warnlog);
		}
	};

	session.videoElement.addEventListener("click", function (e) {
		// show stats of video if double clicked
		log("click");
		try {
			if (e.ctrlKey || e.metaKey) {
				e.preventDefault();

				////////////////////////

				var [menu, innerMenu] = statsMenuCreator();

				//////////////////////////////////

				menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);
				printMyStats(innerMenu);
				e.stopPropagation();

				return false;
			}
		} catch (e) {
			errorlog(e);
		}
	});

	updatePushId();

	/* if (session.directorEnabledPPT){
		enumerateDevices().then(gotDevices).then(async function() {
			console.log("done");
			toggleSettings();
		});
		
		return;
	} */
	//await toggleSettings();

	var constraint = { video: false, audio: true };

	if (session.videoDevice) {
		constraint.video = true;
	}
	if (session.audioDevice === 0) {
		constraint.audio = false;
	}

	requestBasicPermissions(constraint, function () {
		log("requestBasicPermissions done");
		enumerateDevices()
			.then(gotDevices)
			.then(async function () {
				log("enumerateDevices+gotDevices complete");

				pokeIframeAPI("director-share", true, false, session.streamID); // director has started publishing; even if no audio/video.

				log("session.directorEnabledPPT: " + session.directorEnabledPPT);

				if (session.directorEnabledPPT) {
					return;
				}

				if (session.audioDevice !== 0) {
					// change from Auto to Selected Audio Device
					log("SETTING AUDIO DEVICE!!");
					activatedPreview = false;
					await grabAudio("#audioSource3");
				}

				if (session.videoDevice !== 0) {
					activatedPreview = false;
					if (session.quality !== false) {
						await grabVideo(session.quality, "videosource", "#videoSource3");
					} else {
						//session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value);
						await grabVideo(session.roomid ? session.quality_room : session.quality_wb, "videosource", "#videoSource3");
					}
				}

				if (session.videoMutedFlag) {
					session.videoMuted = true;
					toggleVideoMute(true);
				}

				session.directorEnabledPPT = true;
				toggleMute(true);

				//await toggleSettings();

				log("session.seeding: " + session.seeding);

				if (session.seeding) {
					setTimeout(function () {
						if (session.meshcast) {
							meshcast();
						} else if (session.whipOutput) {
							whipOut();
						} else if (session.whepHost) {
							whepOut();
						}
					}, 1000);
					return;
				}

				if (session.autorecord || session.autorecordlocal) {
					log("AUTO RECORD START");
					setTimeout(
						function (v) {
							var videoKbps = session.recordDefault;
							if (session.recordLocal !== false) {
								videoKbps = session.recordLocal;
							}

							if (session.director) {
								recordVideo(document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']"), null, videoKbps);
							} else if (v.stopWriter || v.recording) {
							} else if (v.startWriter) {
								v.startWriter();
							} else {
								recordLocalVideo(null, videoKbps, v);
							}
						},
						2000,
						session.videoElement
					);
				}

				if (session.meshcast) {
					meshcast();
				} else if (session.whipOutput) {
					whipOut();
				} else if (session.whepHost) {
					whepOut();
				}
				
				session.seeding = true;
				await session.seedStream();
				
			});
	});
} // publishdirector

function statsMenuCreator() {
	if (getById("menuStatsBox")) {
		clearInterval(getById("menuStatsBox").interval);
		getById("menuStatsBox").remove();
	}

	var menu = document.createElement("div");
	menu.id = "menuStatsBox";
	menu.className = "debugStats remotestats";
	getById("main").appendChild(menu);

	menu.style.left = parseInt(Math.random() * 10) + 15 + "px";
	menu.style.top = parseInt(Math.random() * 10) + "px";

	menu.innerHTML = "<h1 data-translate='statistics'>Statistics</h1>";
	var menuCloseBtn = document.createElement("button");
	menuCloseBtn.className = "close";
	menuCloseBtn.innerHTML = "×";
	menu.appendChild(menuCloseBtn);

	var innerMenu = document.createElement("div");
	menu.appendChild(innerMenu);

	menuCloseBtn.addEventListener("click", function (eve) {
		clearInterval(menu.interval);
		eve.currentTarget.parentNode.remove();
		eve.preventDefault();
		eve.stopPropagation();
	});
	return [menu, innerMenu];
}

// WEBCAM
session.publishStream = function (v) {
	//  stream is used to generated an SDP
	log("STREAM SETUP");

	if (session.transcript) {
		setTimeout(function () {
			setupClosedCaptions();
		}, 1000);
	}

	if (!session.streamSrc) {
		checkBasicStreamsExist();
	}

	session.streamSrc.oninactive = function streamoninactive() {
		warnlog("Stream inactive");
		if (session.videoElement.recording) {
			session.videoElement.recorder.stop();
		}
	};

	if (session.streamSrc.getVideoTracks().length == 0) {
		warnlog("NO VIDEO TRACK INCLUDED");
	}

	if (session.streamSrc.getAudioTracks().length == 0) {
		warnlog("NO AUDIO TRACK INCLUDED");
	}

	var container = document.createElement("div");
	v.container = container;
	container.id = "container";

	if (session.cleanOutput) {
		container.style.height = "100%";
		v.style.maxWidth = "100%";
		v.style.boxShadow = "none";
	}

	if (session.cover) {
		container.style.setProperty("height", "100%", "important");
	}

	//container.className = "vidcon";
	getById("gridlayout").appendChild(container);

	v.className = "tile"; //"tile task"; TODO: get working  (will add task later on instead)

	v.muted = true;
	v.autoplay = true;
	if (session.showControls !== null) {
		v.controls = session.showControls;
	} else if (session.mobile) {
		v.controls = true;
	} else {
		v.controls = session.showControls || false;
	}
	v.setAttribute("playsinline", "");
	v.id = "videosource"; // could be set to UUID in the future
	v.oncanplay = null;

	session.videoElement = v;

	container.appendChild(v);

	if (session.audioGain !== false) {
		changeMainGain(session.audioGain); // just in case we don't mute things in time via the draw / audioMeter interval
	}

	toggleMute(true);

	if (session.nopreview) {
		v.style.display = "none";
		container.style.display = "none";
	}

	if (((session.roomid === false || session.roomid === "") && session.quality === false) || session.forceMediaSettings) {
		try {
			if (session.quality_wb !== false && session.quality === false) {
				getById("webcamquality3").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : session.quality_wb);
			} else if (session.quality !== false) {
				getById("webcamquality3").elements.namedItem("resolution").value = session.quality;
			}
			getById("gear_webcam3").style.display = "inline-block";
			getById("webcamquality3").onchange = function (event) {
				if (parseInt(getById("webcamquality3").elements.namedItem("resolution").value) == 2) {
					if (session.maxframeRate === false) {
						session.maxframeRate = 30;
						session.maxframeRate_q2 = true;
					}
				} else if (session.maxframeRate_q2) {
					session.maxframeRate = false;
					session.maxframeRate_q2 = false;
				}
				activatedPreview = false;
				session.quality_wb = parseInt(getById("webcamquality3").elements.namedItem("resolution").value);
				session.quality_room = session.quality_wb;
				grabVideo(session.quality_wb, "videosource", "select#videoSource3");
			};
		} catch (e) {
			errorlog(e);
		}
	}

	var bigPlayButton = document.getElementById("bigPlayButton");
	if (bigPlayButton) {
		bigPlayButton.parentNode.removeChild(bigPlayButton);
	}

	if (session.streamID) {
		session.videoElement.dataset.sid = session.streamID;
	}

	if (session.statsMenu) {
		var [menu, innerMenu] = statsMenuCreator();
		menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);
		printMyStats(innerMenu);
	}

	if (session.director) {
		// the director doesn't load a webcam by default anyways.
		// audio is not mucked with
	} else if (session.scene !== false) {
		// it's a scene, and there are no previews in a scene.
		//setTimeout(function(){updateMixer();},10);
	} else if (session.roomid !== false) {
		if (session.roomid === "") {
			if (!session.view || session.view === "") {
				if (session.fullscreen) {
					session.windowed = session.windowed === null ? false : session.windowed;
				} else if (session.minipreview) {
					session.windowed = session.windowed === null ? false : session.windowed;
				} else {
					session.windowed = session.windowed === null ? true : session.windowed;
				}

				if (session.windowed) {
					v.className = "myVideo"; //"myVideo task"; TODO: get working
					container.classList.add("vidcon");
				}

				getById("mutespeakerbutton").classList.add("hidden");

				applyMirror(session.mirrorExclude);

				container.style.width = "100%";
				//container.style.height="100%";

				container.style.alignItems = "center";
				container.backgroundColor = "#666";

				setTimeout(function () {
					dragElement(v);
				}, 1000);
				play();
			} else {
				session.windowed = session.windowed === null ? false : session.windowed;
				applyMirror(session.mirrorExclude);
				play();
				//setTimeout(function(){updateMixer();},10);
			}
		} else {
			//session.cbr=0; // we're just going to override it
			if (session.stereo == 5) {
				// not a scene or director, so we will assume its a guest. changing to stereo=3
				session.stereo = 3;
			}
			session.windowed = session.windowed === null ? false : session.windowed;
			applyMirror(session.mirrorExclude);

			if (session.include.length) {
				play();
			}

			//setTimeout(function(){updateMixer();},10);
		}
	} else {
		if (session.fullscreen) {
			session.windowed = session.windowed === null ? false : session.windowed;
		} else if (session.minipreview) {
			session.windowed = session.windowed === null ? false : session.windowed;
		} else {
			session.windowed = session.windowed === null ? true : session.windowed;
		}
		if (session.windowed) {
			v.className = "myVideo"; //"myVideo task"; TODO: get working
			container.classList.add("vidcon");
		}
		getById("mutespeakerbutton").classList.add("hidden");

		applyMirror(session.mirrorExclude);

		container.style.width = "100%";
		//container.style.height="100%";
		//container.style.display = "flex";

		container.style.alignItems = "center";
		container.backgroundColor = "#666";

		setTimeout(function () {
			dragElement(v);
		}, 1000);
	}

	v.addEventListener("click", function (e) {
		log("click");
		try {
			if (e.ctrlKey || e.metaKey) {
				e.preventDefault();

				var [menu, innerMenu] = statsMenuCreator();

				menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);

				printMyStats(innerMenu);
				e.stopPropagation();
				return false;
			}
		} catch (e) {
			errorlog(e);
		}
	});

	v.touchTimeOut = null;
	v.touchLastTap = 0;
	v.touchCount = 0;

	v.addEventListener("touchend", function (event) {
		if (session.disableMouseEvents) {
			return;
		}
		
	});

	updateReshareLink();
	pokeIframeAPI("started-camera"); // depreciated
	pokeIframeAPI("camera-share", true);

	if (session.videoMutedFlag) {
		session.videoMuted = true;
		toggleVideoMute(true);
	}

	if (!gotDevices2AlreadyRan) {
		enumerateDevices().then(gotDevices2); // this is needed for iOS; was previous set to timeout at 100ms, but would be useful everywhere I think
	}

	v.dataset.menu = "context-menu-video";
	if (!session.cleanOutput) {
		v.classList.add("task"); // this adds the right-click menu
	}

	session.postPublish();

	if (session.autorecord || session.autorecordlocal) {
		log("AUTO RECORD START");
		setTimeout(
			function (v) {
				var videoKbps = session.recordDefault;
				if (session.recordLocal !== false) {
					videoKbps = session.recordLocal;
				}

				if (session.director) {
					recordVideo(document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']"), null, videoKbps);
				} else if (v.stopWriter || v.recording) {
				} else if (v.startWriter) {
					v.startWriter();
				} else {
					recordLocalVideo(null, videoKbps, v);
				}
			},
			2000,
			v
		);
	}

	setTimeout(function () {
		updateMixer();
	}, 10);
}; // publishStream

function stickyMessage(message) {
	var textOverlay = getById("stickyMsgs");
	if (textOverlay) {
		var spanOverlay = document.createElement("span");
		spanOverlay.innerHTML = message;
		var closeBtn = document.createElement("button");
		closeBtn.className = "overlayCloseBtn red";
		closeBtn.innerHTML = "❌";
		closeBtn.title = "Close this message";
		closeBtn.onclick = function () {
			this.parentNode.remove();
		};
		textOverlay.appendChild(spanOverlay);
		spanOverlay.appendChild(closeBtn);
		textOverlay.classList.remove("hidden");
		setTimeout(
			function (spanOverlay) {
				if (spanOverlay) {
					spanOverlay.style.animation = "fadeout 1s";
					spanOverlay.style.opacity = "0";
					setTimeout(
						function (spanOverlay) {
							spanOverlay.remove();
						},
						900,
						spanOverlay
					);
				}
			},
			30000,
			spanOverlay
		);
	}
}

session.postPublish = async function () {
	log("Post publish");
	if (session.welcomeMessage) {
		stickyMessage(session.welcomeMessage);
		// getChatMessage(session.welcomeMessage, false, true, true);
	}
	if (session.queue && (session.queueType == 3 || session.queueType == 4) && !session.director) {
		youreWaitingToBeActivated();
	}

	if (session.welcomeImage) {
		let welcomeoverlay = document.createElement("img");
		welcomeoverlay.src = session.welcomeImage;
		welcomeoverlay.className = "welcomeOverlay";
		document.body.appendChild(welcomeoverlay);
		await sleep(2000);
		setTimeout(
			function (welcomeoverlay) {
				welcomeoverlay.style = "animation: fadeout 1s;";
				setTimeout(
					function (welcomeoverlay) {
						welcomeoverlay.remove();
					},
					990,
					welcomeoverlay
				);
			},
			1000,
			welcomeoverlay
		);
	}

	if (session.welcomeHTML) {
		let welcomeHTML = document.createElement("div");
		welcomeHTML.innerHTML = session.welcomeHTML;
		welcomeHTML.className = "welcomeOverlay";
		document.body.appendChild(welcomeHTML);
		setTimeout(
			function (welcomeHTML) {
				welcomeHTML.style = "animation: fadeout 1s;";
				setTimeout(
					function (welcomeHTML) {
						welcomeHTML.remove();
					},
					990,
					welcomeHTML
				);
			},
			3000,
			welcomeHTML
		);
	}
	
	if (session.waitPage && session.iFramesAllowed) {
		let waitPageIFrame = parseURL4Iframe(session.waitPage);
		if (waitPageIFrame){
			session.layout = combinedLayout([{"w":100,"h":100,"x":0,"y":0,"z":1,"slot":1,"cover":false,"borderThickness":20,"animated":1000,"borderColor":"#0000","backgroundMedia":"","foregroundMedia":"","iframeSrc":waitPageIFrame,"defaultStreamID":"","margin":50,"rounded":30,"muted":false}]);
			updateMixer();
		}
	}
	
	clearInterval(session.updateLocalStatsInterval);
	session.updateLocalStatsInterval = setInterval(function () {
		updateLocalStats();
	}, session.statsInterval);

	pokeIframeAPI("screen-share-state", false);

	session.seeding = true;
	session.seedStream();

	if (session.whipOutput){ // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024
		whipOut();
	}
	if (session.meshcast){
		await meshcast();
	}
	if (session.whepHost){
		whepOut();
	}
	if (session.chunkcast) {
		session.chunkedStream(null);
	}

	if (session.motionRecord && session.videoElement && !session.motionDetectionInterval) {
		session.motionDetectionInterval = setTimeout(function () {
			setInterval(function () {
				motionDetection(session.videoElement, session.motionRecord);
			}, 400);
		}, 2000);
	}
	
	if (session.poke){
		if (session.poke===true){
			let topic = await generateTopic(session.roomid, session.streamID, false, false, session.hash, window.location.hostname);
			await triggerNotification(topic)
		} else {
			await triggerNotification(session.poke);
		}
	}
	
};
function triggerNotification(topic, customMessage = null) {
  if (!topic) return false;
  
  const message = customMessage || ((session.label ? session.label : 'Someone') + 
    (session.roomid ? ' joined your room' : ' joined your stream'));
  
  const notifyUrl = `https://notify.vdo.ninja/?notify=${topic}&message=${encodeURIComponent(message)}`;
  
  console.log('Sending notification to:', notifyUrl);
  
  return fetch(notifyUrl)
    .then(response => {
      console.log('Notification response status:', response.status);
      if (!response.ok) {
        return response.text().then(text => {
          try {
            const errorData = JSON.parse(text);
            console.error('Notification server error:', errorData);
            return false;
          } catch (e) {
            console.error('Notification error response:', text);
            return false;
          }
        });
      }
      
      return response.json();
    })
    .then(data => {
      if (data === false) return false;
      
      console.log('Notification result:', data);
      
      // Check push results to diagnose issues
      if (data.pushResults && Array.isArray(data.pushResults)) {
        data.pushResults.forEach(result => {
          if (!result.success) {
            console.warn('Push notification failed:', result);
          }
        });
      }
      
      return data.success === true;
    })
    .catch(error => {
      console.error('Error sending notification:', error);
      return false;
    });
}
function hashTopic(text) {
  const salt1 = "abc12345ASB234ASD1116";
  const salt2 = "xyzJKL789MNO567PQR890";
  const salt3 = "9843kasdjfh234jhk234j";
  let saltedText = salt1 + text + salt2 + text.split('').reverse().join('') + salt3;
  let hash = 0;
  if (saltedText.length === 0) return "0";
  for (let i = 0; i < saltedText.length; i++) {
    const char = saltedText.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash;
  }
  let hash2 = 0;
  for (let i = 0; i < saltedText.length; i++) {
    hash2 = ((hash2 << 7) + hash2) + saltedText.charCodeAt(i);
    hash2 = hash2 & hash2;
  }
  const combinedHash = Math.abs(hash).toString(36) + Math.abs(hash2).toString(36);
  if (combinedHash.length < 10) {
    return combinedHash + Math.random().toString(36).substring(2, 12);
  }
  return combinedHash;
}

async function generateTopic(roomId, pushId, viewId, password, hash, domain) {
  domain = domain || 'vdo.ninja';
  if (!roomId && !viewId && !pushId) {
    console.error('At least one of roomId, viewId or pushId is required');
    return null;
  }
  const components = {
    room: roomId || viewId || pushId,
    domain: domain.replace(/\./g, '_')
  };
  let sensitiveData = Object.values(components).filter(Boolean).join('_');
  if (hash) {
    sensitiveData += `_${hash}`;
  } else if (password) {
    const passwordHash = await generateHash(password);
    sensitiveData += `_${passwordHash}`;
  }
  const secureTopicHash = hashTopic(sensitiveData);
  const finalPrefix = components.domain;
  const finalTopic = `${finalPrefix}_${secureTopicHash}`;
  return finalTopic;
}

async function publishScreen2(constraints, audioList = [], audio = true, overrideFramerate = false) {
	// webcam stream is used to generated an SDP
	log("SCREEN SHARE SETUP - publishScreen2");

	if (!navigator.mediaDevices.getDisplayMedia) {
		setTimeout(function () {
			if (iOS || iPad) {
				warnUser("Sorry, but your iOS browser does not support screen-sharing.\n\nPlease see <a href='https://docs.vdo.ninja/guides/screen-share-your-iphone-ipad' target='_blank'>this guide</a> for an alternative method to do so.", false, false);
			} else if (session.mobile) {
				warnUser("Sorry, your browser does not support screen-sharing.\n\nThe <a href='https://docs.vdo.ninja/getting-started/native-mobile-app-versions#android-download-link' target='_blank'>Android native app</a> should support it though.", false, false);
			} else {
				warnUser("Sorry, your browser does not support screen-sharing.\n\nPlease use the desktop versions of Firefox or Chrome instead.");
			}
		}, 1);
		return false;
	}
	if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
		if (!ElectronDesktopCapture) {
			if (!(session.cleanOutput && session.cleanish == false)) {
				warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)");
			}
			return false;
		}
	}

	var streams = [];
	log(audioList);
	for (var i = 0; i < audioList.length; i++) {
		// mic sources; not screen .
		let constraintAudio = { video: false, audio: { deviceId: { exact: audioList[i] } } };

		if (session.echoCancellation === false) {
			// default should be ON.  we won't even add it since deviceId is specified and Browser defaults to on already
			constraintAudio.audio.echoCancellation = false;
		} else {
			constraintAudio.audio.echoCancellation = true;
		}
		if (session.autoGainControl === false) {
			constraintAudio.audio.autoGainControl = false;
		} else {
			constraintAudio.audio.autoGainControl = true;
		}
		if (session.noiseSuppression === false) {
			constraintAudio.audio.noiseSuppression = false;
		} else {
			constraintAudio.audio.noiseSuppression = true;
		}
		
		if (session.voiceIsolation === true) {
			constraintAudio.audio.voiceIsolation = true;
		}

		if (session.audioInputChannels) {
			if (constraintAudio.audio === true) {
				constraintAudio.audio = {};
				constraintAudio.audio.channelCount = session.audioInputChannels;
			} else if (constraintAudio.audio) {
				constraintAudio.audio.channelCount = session.audioInputChannels;
			}
		}

		if (session.micSampleRate) {
			if (constraintAudio.audio === true) {
				constraintAudio.audio = {};
				constraintAudio.audio.sampleRate = parseInt(session.micSampleRate);
			} else if (constraintAudio.audio) {
				constraintAudio.audio.sampleRate = parseInt(session.micSampleRate);
			}
		}
		if (session.micSampleSize) {
			if (constraintAudio.audio === true) {
				constraintAudio.audio = {};
				constraintAudio.audio.sampleSize = parseInt(session.micSampleSize);
			} else if (constraintAudio.audio) {
				constraintAudio.audio.sampleSize = parseInt(session.micSampleSize);
			}
		}
		getUserMediaRequestID += 1;
		var gumID = getUserMediaRequestID;
		
		if (Firefox){
			constraintAudio = toFirefoxConstraint(constraintAudio);
		}
		
		log(constraintAudio);
		
		warnlog("navigator.mediaDevices.getUserMedia starting...");
		await navigator.mediaDevices
			.getUserMedia(constraintAudio)
			.then(stream => {
				if (getUserMediaRequestID !== gumID) {
					warnlog("GET USER MEDIA CALL HAS EXPIRED 3");
					stream.getTracks().forEach(function (track) {
						stream.removeTrack(track);
						track.stop();
						log("stopping old track");
					});
					return;
				}
				streams.push(stream);
			})
			.catch(errorlog);
	}

	if (session.audioDevice === 0) {
		constraints.audio = false;
	}

	if (session.screenshareVideoOnly) {
		constraints.audio = false;
	}

	if (constraints.video !== false && Object.keys(constraints.video).length == 0) {
		constraints.video = true;
	}

	log(constraints);
	getUserMediaRequestID += 1;
	var gumID = getUserMediaRequestID;
	return navigator.mediaDevices
		.getDisplayMedia(constraints)
		.then(async function (stream) {
			if (getUserMediaRequestID !== gumID) {
				warnlog("GET USER MEDIA CALL HAS EXPIRED 3");
				stream.getTracks().forEach(function (track) {
					stream.removeTrack(track);
					track.stop();
					log("stopping old track");
				});
				return;
			}

			try {
				var constraint = {};

				if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) {
					constraint.aspectRatio = parseFloat(session.forceAspectRatio);
				} else if (session.forceScreenShareAspectRatio) {
					constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio);
				}
				if (overrideFramerate) {
					constraint.frameRate = overrideFramerate;
				}
				if (Object.keys(constraint).length) {
					await stream.getVideoTracks()[0].applyConstraints({
						advanced: [constraint]
					});
					log({
						advanced: [constraint]
					});
				}
			} catch (e) {
				errorlog(e);
			}

			/// RETURN stream for preview? rather than jumping right in.
			session.screenShareState = true;
			pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
			notifyOfScreenShare();

			try {
				stream.getVideoTracks()[0].onended = function () {
					toggleScreenShare();
				};
			} catch (e) {
				log("No Video selected; screensharing?");
			}

			// OR, jump right in, and let user change from there
			if (session.roomid !== false) {
				if (session.roomid === "" && (!session.view || session.view === "")) {
					if (session.manual === null) {
						session.manual = session.manual === null ? true : session.manual;
					}
					if (!session.cleanOutput) {
						var showReshare = getStorage("showReshare");
						if (showReshare) {
							generateHash(session.streamID + session.salt + "bca321", 4)
								.then(function (hash) {
									// million to one error.
									if (showReshare === hash) {
										getById("head3").classList.remove("hidden");
										getById("head3a").classList.remove("hidden");
									} else if (session.permaid === null) {
										getById("head3").classList.remove("hidden");
										getById("head3a").classList.remove("hidden");
									}
								})
								.catch(errorlog);
						}
					}
				} else {
					getById("head3").classList.add("hidden");
					getById("head3a").classList.add("hidden");
					log("ROOMID EANBLED");
					log("Update Mixer Event on REsize SET");
					window.onresize = updateMixer;
					window.onorientationchange = function () {
						if (Firefox) {
							updateForceRotate(true);
						}
						setTimeout(async function () {
							if (session.forceAspectRatio) {
								await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
							}
							if (session.effect && session.effect === "7") {
								digitalZoom();
							}
							updateForceRotate();
							updateMixer();
						}, 200);
					};
					joinRoom(session.roomid);
				}
			} else {
				getById("head3").classList.remove("hidden");
				getById("head3a").classList.remove("hidden");
				getById("logoname").style.display = "none";
			}

			updatePushId();

			if (stream.getAudioTracks().length) {
				screenShareAudioTrack = stream.getAudioTracks()[0];
			}

			log("adding tracks");
			for (var i = 0; i < streams.length; i++) {
				streams[i].getAudioTracks().forEach(track => {
					stream.addTrack(track);
				});
			}
			streams = null;
			if (!session.screenshareVideoOnly && session.audioDevice !== 0) {
				if (stream.getAudioTracks().length == 0) {
					if (!session.cleanOutput) {
						if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
							// Electron has no audio.
						} else if (!getStorage("leaveInPeaceSSWarning")){
							setStorage("leaveInPeaceSSWarning" ,"true", 720);
							setTimeout(function () {
								warnUser(getTranslation("no-audio-source-detected"), 10000, false);
							}, 300);
						}
					}
				}
			}

			try {
				session.streamSrc = stream;
			} catch (e) {
				errorlog(e);
			}
			toggleMute(true);

			var v = createVideoElement();
			session.videoElement = v;

			if (session.streamID) {
				session.videoElement.dataset.sid = session.streamID;
			}

			var container = document.createElement("div");
			v.container = container;
			container.id = "container_screen";
			container.style.height = "100%";

			if (session.cleanOutput) {
				v.style.maxWidth = "100%";
				v.style.boxShadow = "none";
			}

			//container.className = "vidcon";
			getById("gridlayout").appendChild(container);

			if (session.nopreview) {
				v.style.display = "none";
				container.style.display = "none";
			}

			//if (session.cover){
			//	container.style.setProperty('height', '100%', 'important');
			//}

			container.appendChild(v);

			v.className = "tile";

			if (session.director) {
			} else if (session.scene !== false) {
				setTimeout(function () {
					updateMixer();
				}, 1);
			} else if (session.roomid !== false) {
				if (session.roomid === "") {
					if (!session.view || session.view === "") {
						getById("mutespeakerbutton").classList.add("hidden");

						if (session.fullscreen) {
							session.windowed = session.windowed === null ? false : session.windowed;
						} else {
							session.windowed = session.windowed === null ? true : session.windowed;
						}

						if (!session.windowed) {
							if (session.mirrored && session.flipped) {
								v.style.transform = " scaleX(-1) scaleY(-1)";
								v.classList.add("mirrorControl");
							} else if (session.mirrored) {
								v.style.transform = "scaleX(-1)";
								v.classList.add("mirrorControl");
							} else if (session.flipped) {
								v.style.transform = "scaleY(-1)";
								v.classList.remove("mirrorControl");
							} else {
								v.style.transform = "";
								v.classList.remove("mirrorControl");
							}
						} else {
							v.className = "myVideo";
							if (session.mirrored && session.flipped) {
								v.style.transform = " scaleX(-1) scaleY(-1) translate(0, 50%)";
								v.classList.add("mirrorControl");
							} else if (session.mirrored) {
								v.style.transform = "scaleX(-1) translate(0, -50%)";
								v.classList.add("mirrorControl");
							} else if (session.flipped) {
								v.style.transform = "scaleY(-1) translate(0, 50%)";
								v.classList.remove("mirrorControl");
							} else {
								v.style.transform = " translate(0, -50%)";
								v.classList.remove("mirrorControl");
							}
						}

						container.style.width = "100%";
						//container.style.height="100%";
						container.style.alignItems = "center";
						container.backgroundColor = "#666";

						setTimeout(function () {
							dragElement(v);
						}, 1000);
						play();
					} else {
						play();
						setTimeout(function () {
							updateMixer();
						}, 1);
					}
				} else {
					setTimeout(function () {
						updateMixer();
					}, 1);
				}
			} else {
				getById("mutespeakerbutton").classList.add("hidden");
				if (session.fullscreen) {
					session.windowed = session.windowed === null ? false : session.windowed;
				} else {
					session.windowed = session.windowed === null ? true : session.windowed;
				}
				if (!session.windowed) {
					if (session.mirrored && session.flipped) {
						v.style.transform = " scaleX(-1) scaleY(-1)";
						v.classList.add("mirrorControl");
					} else if (session.mirrored) {
						v.style.transform = "scaleX(-1)";
						v.classList.add("mirrorControl");
					} else if (session.flipped) {
						v.style.transform = "scaleY(-1)";
						v.classList.remove("mirrorControl");
					} else {
						v.style.transform = "";
						v.classList.remove("mirrorControl");
					}
				} else {
					v.className = "myVideo";
					container.classList.add("vidcon");
					if (session.mirrored && session.flipped) {
						v.style.transform = " scaleX(-1) scaleY(-1) translate(0, 50%)";
						v.classList.add("mirrorControl");
					} else if (session.mirrored) {
						v.style.transform = "scaleX(-1) translate(0, -50%)";
						v.classList.add("mirrorControl");
					} else if (session.flipped) {
						v.style.transform = "scaleY(-1) translate(0, 50%)";
						v.classList.remove("mirrorControl");
					} else {
						v.style.transform = " translate(0, -50%)";
						v.classList.remove("mirrorControl");
					}
				}

				container.style.width = "100%";
				//container.style.height="100%";
				container.style.alignItems = "center";
				container.backgroundColor = "#666";
			}

			if (!session.windowed) {
				window.onresize = updateMixer;
				window.onorientationchange = function () {
					if (Firefox) {
						updateForceRotate(true);
					}
					setTimeout(async function () {
						if (session.forceAspectRatio) {
							await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
						}
						if (session.effect && session.effect === "7") {
							digitalZoom();
						}
						updateForceRotate();
						updateMixer();
					}, 200);
				};
			}

			v.autoplay = true;
			v.controls = session.showControls || false;
			v.setAttribute("playsinline", "");
			v.muted = true;
			v.id = "videosource";
			v.dataset.menu = "context-menu-video";

			if (!session.cleanOutput) {
				v.classList.add("task"); // this adds the right-click menu
			}

			//if (!v.srcObject || v.srcObject.id !== stream.id) {
			//	v.srcObject = stream;
			v.srcObject = outboundAudioPipeline();
			//}

			v.onpause = event => {
				// prevent things from pausing; human or other
				if (!(event.ctrlKey || event.metaKey)) {
					log("Video paused; auto playing");
					event.currentTarget
						.play()
						.then(_ => {
							log("playing 11");
						})
						.catch(warnlog);
				}
			};

			v.addEventListener("click", function (e) {
				// show stats of video if double clicked
				log("click");
				try {
					if (e.ctrlKey || e.metaKey) {
						e.preventDefault();

						var [menu, innerMenu] = statsMenuCreator();

						menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);

						printMyStats(innerMenu);
						e.stopPropagation();
						return false;
					}
				} catch (e) {
					errorlog(e);
				}
			});

			updateReshareLink();

			if (session.videoMutedFlag) {
				session.videoMuted = true;
				toggleVideoMute(true);
			}

			clearInterval(session.updateLocalStatsInterval);
			session.updateLocalStatsInterval = setInterval(function () {
				updateLocalStats();
			}, session.statsInterval);

			if (session.whipOutput){ // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024
				whipOut();
			}
			if (session.meshcast){
				await meshcast();
			}
			if (session.whepHost){
				whepOut();
			}
			
			session.seeding = true;
			session.seedStream();

			//pokeIframeAPI('started-screenshare'); // depreciated
			pokeIframeAPI("screen-share-state", true, null, session.streamID); // (action, value = null, UUID = null, SID=null)

			if (session.autorecord || session.autorecordlocal) {
				log("AUTO RECORD START");
				setTimeout(
					function (v) {
						var videoKbps = session.recordDefault;
						if (session.recordLocal !== false) {
							videoKbps = session.recordLocal;
						}

						if (session.director) {
							recordVideo(document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']"), null, videoKbps);
						} else if (v.stopWriter || v.recording) {
						} else if (v.startWriter) {
							v.startWriter();
						} else {
							recordLocalVideo(null, videoKbps, v);
						}
					},
					2000,
					v
				);
			}

			return true;
		})
		.catch(function (err) {
			errorlog(err);
			errorlog(err.name);
			if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") {
				// User Stopped it.  (is this next part needed??)
				session.screenShareState = false;
				pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
				notifyOfScreenShare();

				if (macOS) {
					warnUser(getTranslation("screen-permissions-denied"), false, false);
				}
				return false;
			} else {
				if (audio == true) {
					if (err.name == "NotReadableError") {
						if (!session.cleanOutput) {
							warnUser(getTranslation("change-audio-output-device"), false, false);
						}
						constraints.audio = false;
						return publishScreen2(constraints, audioList, false);
					} else {
						constraints.audio = false;
						if (!session.cleanOutput) {
							setTimeout(function () {
								warnUser(err);
							}, 1); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported
						}
						return publishScreen2(constraints, audioList, false);
					}
				} else {
					if (!session.cleanOutput) {
						setTimeout(function () {
							warnUser(err);
						}, 1); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported
					}
					return false;
				}
			}
		});
} // publishStream2

var transferList = [];
var msgTransferList = [];

function cancelFile(ele) {
	var idx = ele.dataset.tid;
	try {
		transferList[idx].dc.close();
	} catch (e) {}
	transferList[idx].status = 5;
	updateDownloadLink(idx);
}

function requestFile(ele) {
	var idx = ele.dataset.tid;
	transferList[idx].status = 1;

	var fid = ele.dataset.fid;
	var UUID = ele.dataset.uuid;
	var msg = {};
	msg.requestFile = fid;
	msg.UUID = UUID;
	session.sendRequest(msg, msg.UUID);

	updateDownloadLink(idx);
	pokeIframeAPI("request-file", fid, UUID);
}

function clearDownloadFile(ele) {
	var idx = ele.dataset.tid;
	transferList[idx].status = 6;
	updateDownloadLink(idx);
}

function addDownloadLink(fileList, UUID, pc) {
	if (session.nodownloads) {
		return;
	} // downloads are blocked
	log(fileList);
	if (!fileList || !fileList.length) {
		return;
	}
	for (var i = 0; i < fileList.length; i++) {
		fileList[i].UUID = UUID;
		fileList[i].completed = 0;
		fileList[i].status = 0;
		fileList[i].time = Date.now();
		fileList[i].pc = pc[UUID];
		transferList.push(fileList[i]);
	}

	if (session.chatbutton === false) {
		return;
	} // messages can still appear as overlays

	updateMessages(); 

	if (session.beepToNotify) {
		playtone();
	}

	if (session.chat == false) {
		getById("chattoggle").className = "las la-comments toggleSize pulsate";
		getById("chatbutton").className = "float";

		if (getById("chatNotification").value) {
			getById("chatNotification").value = getById("chatNotification").value + 1;
		} else {
			getById("chatNotification").value = 1;
		}
		getById("chatNotification").classList.add("notification", "red");
	}

	//if (session.broadcastChannel !== false) {
	//	session.broadcastChannel.postMessage(data); /* send */
	//}
}

function updateDownloadLink(idx) {
	idx = parseInt(idx);
	var elements = document.querySelectorAll('[data-tid="' + idx + '"]');
	if (elements[0]) {
		if (transferList[idx].status === 0) {
			elements[0].innerHTML = "Download it here";
		} else if (transferList[idx].status === 1) {
			elements[0].innerHTML = "Requested";
			//elements[0].onclick='cancelFile(this);'
		} else if (transferList[idx].status === 2) {
			elements[0].innerHTML = "Downloading: " + parseInt(transferList[idx].completed * 100) + "%";
			elements[0].onclick = function () {
				cancelFile(this);
			};
		} else if (transferList[idx].status === 3) {
			elements[0].innerHTML = "Completed";
			elements[0].onclick = null;
			elements[0].disabled = true;
		} else if (transferList[idx].status === 4) {
			elements[0].innerHTML = "No longer available";
			elements[0].onclick = null;
			elements[0].disabled = true;
		} else if (transferList[idx].status === 5) {
			elements[0].innerHTML = "Cancelled";
			elements[0].onclick = null;
			elements[0].disabled = true;
		} else if (transferList[idx].status === 6) {
			getById("transfer_" + idx).style.display = "none";
			//delete(transferList[idx]);
		}
	}
}

function showDownloadLinks() {
	if (session.nodownloads) {
		return;
	} // downloads are blocked
	msgTransferList = [];
	if (!transferList || !transferList.length) {
		return;
	}
	for (var i = 0; i < transferList.length; i++) {
		fileShareMessage(transferList[i], i);
	}
}

function fileShareMessage(fileinfo, idx) {
	fileinfo.name = sanitizeChat(fileinfo.name); // keep it clean.

	var label = false;
	if (fileinfo.pc) {
		if (fileinfo.pc.label) {
			label = sanitizeLabel(fileinfo.pc.label);
		}
	}
	var data = {};
	data.idx = idx;
	if (fileinfo.status === 0) {
		data.msg = " has a shared a file with you:<br /><i>" + fileinfo.name + "</i><br />Do you trust them? <button title='file size: " + fileinfo.size + " bytes' data-button-type='download' data-fid='" + fileinfo.id + "' data-tid='" + idx + "' onclick='requestFile(this);'>Download it here</button><button data-button-type='clear' data-fid='" + fileinfo.id + "' data-tid='" + idx + "' style='margin:10px 0 10px 2px;' onclick='clearDownloadFile(this);'>Clear</button>";
	} else if (fileinfo.status === 1) {
		data.msg = " has a shared a file with you:<br /><i>" + fileinfo.name + "</i><br /><button title='file size: " + fileinfo.size + " bytes' data-button-type='download' data-fid='" + fileinfo.id + "' data-tid='" + idx + "'>Requested</button>";
	} else if (fileinfo.status === 2) {
		data.msg = " has a shared a file with you:<br /><i>" + fileinfo.name + "</i><br /><button title='file size: " + fileinfo.size + " bytes' data-button-type='download' data-fid='" + fileinfo.id + "' data-tid='" + idx + "' onclick='cancelFile(this);'>Downloading: " + parseInt(transferList[idx].completed * 100) + "%</button>";
	} else if (fileinfo.status === 3) {
		data.msg = " has a shared a file with you:<br /><i>" + fileinfo.name + "</i><br /><button disabled title='file size: " + fileinfo.size + " bytes' data-button-type='download' data-fid='" + fileinfo.id + "' data-tid='" + idx + "' >Download Completed</button>";
		transferList[idx].status = 6;
	} else if (fileinfo.status === 4) {
		data.msg = " has a shared a file with you:<br /><i>" + fileinfo.name + "</i><br /><button title='file size: " + fileinfo.size + " bytes' data-button-type='download' data-fid='" + fileinfo.id + "' data-tid='" + idx + "' disabled >No longer available</button>";
	} else if (fileinfo.status === 5) {
		data.msg = " has a shared a file with you:<br /><i>" + fileinfo.name + "</i><br /><button title='file size: " + fileinfo.size + " bytes' data-button-type='download' data-fid='" + fileinfo.id + "' data-tid='" + idx + "' disabled >Cancelled</button>";
		transferList[idx].status = 6;
	} else if (fileinfo.status === 6) {
		return;
	}

	var director = false; // add back in later.
	if (session.directorList.indexOf(fileinfo.UUID) >= 0) {
		director = true;
	}
	if (label) {
		data.label = label;
		if (director) {
			data.label = "<b><i>" + data.label + "</i></b>";
		} else {
			data.label = "<b>" + data.label + "</b>";
		}
	} else if (director) {
		data.label = "<b><i>Director</i></b>";
	} else {
		data.label = "Someone";
	}
	data.type = "action";

	msgTransferList.push(data);
}

session.shareFile = function(ele, UUID=false, event=false) {
    const file = ele.files[0];
    if (!file) return;
    
    try {
        const fileId = session.generateStreamID(7);
        
        session.hostedFiles.push({
            id: fileId,
            name: file.name,
            size: file.size,
			state: 1,
            restricted: UUID || false,
            file: file  // Store the actual File object
        });
        
        log(session.hostedFiles);
		
		enhancedFileTransfers.initTransfer(fileId, file.size);
        
        // Provide file list to appropriate peers
        if (UUID === false) {
            for (let peerUUID in session.pcs) {
                session.provideFileList(peerUUID);
            }
            for (let peerUUID in session.rpcs) {
                if (!(peerUUID in session.pcs)) {
                    session.provideFileList(peerUUID);
                }
            }
        } else {
            session.provideFileList(UUID);
        }
        
        pokeIframeAPI("file-share", true);
        
        // Update the file share display
        updateFileShare(); 
        
        closeModal();
        
    } catch(e) {
        errorlog(e);
    } finally {
        // Clear the file input
        ele.value = '';
    }
};

function arrayBufferToString(buffer, encoding, callback) {
	var blob = new Blob([buffer], { type: "text/plain" });
	var reader = new FileReader();
	reader.onload = function (evt) {
		callback(evt.target.result);
	};
	reader.readAsText(blob, encoding);
}
session.hostFile = function (ele, event=false) {
	// webcam stream is used to generated an SDP
	log("FILE TRANSFER SETUP");
	session.hostedFiles = [];
	
	for (var i = 0; i < ele.files.length; i++) {
		session.hostedFiles.push({
			id: session.generateStreamID(7),
			name: ele.files[i].name,
			size: ele.files[i].size,
			state: 1,
			restricted: false,
			file: ele.files[i]  // Store the actual File object
		});
	}
	
	log(session.hostedFiles);
	
	var container = document.createElement("div");
	container.id = "container_host";
	getById("gridlayout").appendChild(container);

	if (session.cover) {
		container.style.setProperty("height", "100%", "important");
	}

	if (session.roomid !== false) {
		if (session.roomid === "" && (!session.view || session.view === "")) {
		} else {
			log("ROOMID EANBLED");
			//log("Update Mixer Event on REsize SET");
			//window.addEventListener("resize", updateMixer);// TODO FIX
			//window.addEventListener("orientationchange", updateMixer);// TODO FIX
			getById("head3").classList.add("hidden");
			getById("head3a").classList.add("hidden");
			joinRoom(session.roomid);
		}
	} else {
		getById("head3").classList.remove("hidden");
		getById("head3a").classList.remove("hidden");
		getById("logoname").style.display = "none";
	}
	getById("head1").className = "hidden";

	getById("head1").className = "hidden";
	getById("head2").className = "hidden";

	if (!session.cleanOutput) {
		getById("chatbutton").className = "float";
		getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though.
		// getById("mediafileshare").classList.remove("hidden");
		getById("hangupbutton").className = "float";
		getById("controlButtons").classList.remove("hidden");
		//getById("helpbutton").style.display = "inherit";
		//getById("reportbutton").style.display = "";
	} else {
		getById("controlButtons").classList.add("hidden");
	}
	
	updatePushId();
	updateReshareLink();
	updateFileShare();

	pokeIframeAPI("file-share", true);
	pokeIframeAPI("started-fileshare"); // deprecated

	clearInterval(session.updateLocalStatsInterval);
	session.updateLocalStatsInterval = setInterval(function () {
		updateLocalStats();
	}, session.statsInterval);

	session.seeding = true;
	session.seedStream();
	
};

function updateReshareLink() {
	try {
		var m = getById("mainmenu");
		m.remove();
		document.querySelectorAll(".hidden2").forEach(ele2 => {
			ele2.classList.remove("hidden2");
		});
	} catch (e) {}

	var added = "";
	if (session.defaultPassword === false) {
		if (session.password !== false) {
			added = "&pw=" + session.password;
		} else {
			added = "&pw=false";
		}
	}

	var wss = "";
	if (session.wssSetViaUrl) {
		if (session.customWSS && session.customWSS !== true) {
			wss = "&pie=" + session.customWSS;
		} else if (session.customWSS==true){
			wss = "&wss=" + session.wss;
		} else {
			wss = "&wss2=" + session.wss;
		}
	}
	if (session.audience && !session.audienceToken) {
		if (document.getElementById("reshare")) {
			if (!session.cleanOutput) {
				getById("copythisurl").innerHTML = "";
				getById("head3a").classList.remove("hidden");
			}
			document.getElementById("reshare").href = null;
			document.getElementById("reshare").text = "loading public view link...";
			document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px";
		}
		return;
	} else if (session.audience) {
		var shareLink = "https://" + location.host + location.pathname + "?view=" + session.streamID + added + wss + "&audience=" + session.audienceToken;
		if (document.getElementById("reshare")) {
			if (!session.cleanOutput) {
				getById("head3").classList.remove("hidden");
				getById("head3a").classList.remove("hidden");
				getById("copythisurl").innerHTML = '<span data-translate="copy-audience-url">This is your public audience link</span> <i style="color: #CCC;" class="las la-long-arrow-alt-right"></i> &nbsp; ';
			}
			document.getElementById("reshare").href = shareLink;
			document.getElementById("reshare").text = shareLink;
			document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px";
		}
		pokeIframeAPI("share-link", shareLink);
		return;
	}
	var shareLink = "https://" + location.host + location.pathname + "?view=" + session.streamID + added + wss;
	if (document.getElementById("reshare")) {
		document.getElementById("reshare").href = shareLink;
		document.getElementById("reshare").text = shareLink;
		document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px";
	}
	pokeIframeAPI("share-link", shareLink);
}

function cleanupMediaState(vid) {
    if (session.canvasSource) {
        session.canvasSource.destroy();
        session.canvasSource = null;
    }
    
    if (vid) {
        if (vid.srcObject) {
            const tracks = vid.srcObject.getTracks();
            tracks.forEach(track => track.stop());
            vid.srcObject = null;
        }
        if (vid.currentObjectURL) {
            URL.revokeObjectURL(vid.currentObjectURL);
            vid.currentObjectURL = null;
        }
        vid.src = '';
    }
    
    if (session.streamSrc) {
        const tracks = session.streamSrc.getTracks();
        tracks.forEach(track => track.stop());
        session.streamSrc = null;
    }
}

session.changePublishFile = function (ele, event) {
    log("FILE STREAM CHANGE");
    var files = Array.from(ele.files);
    var vid = getById("videosource");
    
    // Clean up existing state first
    cleanupMediaState(vid);
    
    if (files[0].type.startsWith('image/')) {
        const objectURL = URL.createObjectURL(files[0]);
        session.canvasSource = new CanvasStreamSource();
        session.canvasSource.imgSrc = objectURL;
        window.postMessage({ type: 'canvas-frame', frame: session.canvasSource.imgSrc }, '*');
        
        session.streamSrc = session.canvasSource.getStream();
        // Process the stream like we do for video
        session.streamSrc = outboundAudioPipeline(session.streamSrc);
        
        var tracks = session.streamSrc.getVideoTracks();
        if (tracks.length) {
            pushOutVideoTrack(tracks[0]);
        }
        senderAudioUpdate();
        
        vid.play();  // We still need play() but don't set srcObjec
    } else {
        log("FILE VIDEO STREAM CHANGE");
        vid.playlist = files;
        nextFilePlaylist(vid);
    }
    session.applySoloChat();
    session.applyIsolatedChat(); 
    toggleMute(true);
};

function nextFilePlaylist(vid) {
    log("nextFilePlaylistD");
    var filenext = vid.playlist.shift();
    
    cleanupMediaState(vid);
    
    if (!filenext) {
        if (session.blankStream) {
            session.streamSrc = session.blankStream;
            pushOutVideoTrack(session.blankStream.getVideoTracks()[0]);
        }
        return;
    }
    
    vid.pause();
    vid.currentObjectURL = URL.createObjectURL(filenext);
    vid.src = vid.currentObjectURL;
    
	 // Wrap the onloadeddata in error handling
    vid.onloadeddata = function() {
        try {
            if (Firefox) {
                session.streamSrc = vid.mozCaptureStream();
            } else {
                session.streamSrc = vid.captureStream();
            } 
            updateMixer();
            var tracks = session.streamSrc.getVideoTracks();
            if (tracks.length) {
                pushOutVideoTrack(tracks[0]); // video only
            }
			senderAudioUpdate(false, session.streamSrc);
			
        } catch (e) {
            errorlog(e);
            if (session.blankStream) {
                session.streamSrc = session.blankStream;
                pushOutVideoTrack(session.blankStream.getVideoTracks()[0]);
            }
        }
    };

    vid.onerror = (e) => {
        errorlog(e);
		cleanupMediaState(vid);
        if (session.blankStream) {
            session.streamSrc = session.blankStream;
            pushOutVideoTrack(session.blankStream.getVideoTracks()[0]);
        }
        // Try next file if available
        if (vid.playlist.length) {
            setTimeout(() => nextFilePlaylist(vid), 1000);
        }
    };
    
    vid.load();
    
    vid.play()
        .then(_ => {
            log("playing 2");
        })
        .catch(e => {
            warnlog(e);
            if (session.blankStream) {
                session.streamSrc = session.blankStream;
                pushOutVideoTrack(session.blankStream.getVideoTracks()[0]);
            }
            // Try next file if available
            if (vid.playlist.length) {
                setTimeout(() => nextFilePlaylist(vid), 1000);
            }
        });
}


session.publishFile = function (ele, event) {
	log("FILE STREAM SETUP");
	
	if (!session.blankStream) {
		const canvas = document.createElement('canvas');
		canvas.width = 1280;
		canvas.height = 720;
		const ctx = canvas.getContext('2d');
		ctx.fillStyle = 'black';
		ctx.fillRect(0, 0, canvas.width, canvas.height);
		
		session.blankStream = canvas.captureStream(30);
		session.streamSrc = session.blankStream;
	}
	
	if (session.transcript) {
		setTimeout(function () {
			setupClosedCaptions();
		}, 1000);
	}

	const files = Array.from(ele.files);
	log(files);
	const file = files[0];
	const isImage = file.type.startsWith('image/');

	var fileURL = URL.createObjectURL(file);
	var container = document.createElement("div");
	container.id = "container";

	if (session.cover) {
		container.style.setProperty("height", "100%", "important");
	}

	var v = createVideoElement();
	v.container = container;
	
	if (session.cleanOutput) {
		container.style.height = "100%";
		v.style.maxWidth = "100%";
		v.style.boxShadow = "none";
	}
	
	container.appendChild(v);

	if (session.streamID) {
		v.dataset.sid = session.streamID;
	}

	v.autoplay = false;
	if (session.showControls !== null) {
		v.controls = session.showControls;
	} else {
		v.controls = true;
	}
	v.muted = false;
	v.loop = files.length == 1;

	v.id = "videosource";
	v.dataset.menu = "context-menu-video";
	v.setAttribute("playsinline", "");
	
	if (isImage) {
		
		session.canvasSource = new CanvasStreamSource();
		session.canvasSource.imgSrc = fileURL;
		
		window.postMessage({ type: 'canvas-frame', frame: session.canvasSource.imgSrc }, '*');
		
		session.streamSrc = session.canvasSource.getStream();
		v.srcObject = session.streamSrc;
		v.play();
		handleUIAndStream();
	} else {
		v.src = fileURL;
	}
	
	v.playlist = files;
	v.addEventListener("ended", function(e) {
		log("MY HANDLER TRIGGERED");
		var vid = getById("videosource");
		nextFilePlaylist(vid);
	}, false);

	v.className = "tile clean fileshare";
	
	session.videoElement = v;
	session.mirrorExclude = true;

	v.addEventListener("click", (e) => {
		log("click");
		try {
			if (e.ctrlKey || e.metaKey) {
				e.preventDefault();

				var [menu, innerMenu] = statsMenuCreator();

				menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);

				printMyStats(innerMenu);
				e.stopPropagation();
				return false;
			}
		} catch (e) {
			errorlog(e);
		}
	});

	v.touchTimeOut = null;
	v.touchLastTap = 0;
	v.touchCount = 0;
	
	v.addEventListener("touchend", (event) => {
		if (session.disableMouseEvents) {
			return;
		}
		log("touched");

		document.onmousemove = null;
		document.ontouchmove = null;

		var currentTime = new Date().getTime();
		var tapLength = currentTime - v.touchLastTap;
		clearTimeout(v.touchTimeOut);
		if (tapLength < 500 && tapLength > 0) {
			log("double touched");
			v.touchCount += 1;
			event.preventDefault();
			if (v.touchCount < 5) {
				v.touchLastTap = currentTime;
				return false;
			}
			v.touchLastTap = 0;
			v.touchCount = 0;

			var [menu, innerMenu] = statsMenuCreator();

			menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);

			printMyStats(innerMenu);
			event.stopPropagation();
			return false;
		} else {
			v.touchCount = 1;
			v.touchTimeOut = setTimeout(
				function (vv) {
					clearTimeout(vv.touchTimeOut);
					vv.touchLastTap = 0;
					vv.touchCount = 0;
				},
				5000,
				v
			);
			v.touchLastTap = currentTime;
		}
	});
	
	v.onerror = () => {
		if (session.cleanOutput) {
			errorlog("File failed to load.\n\nSelect a compatible media file.");
		} else {
			warnUser("File failed to load.\n\nSelect a compatible media file.");
		}
	};

	v.onloadeddata = async () => {
		session.mediafileShare = true;
		getById("mainmenu").remove();
		
		if (Firefox) {
			session.streamSrc = v.mozCaptureStream();
		} else {
			session.streamSrc = v.captureStream();
		}
		
		handleUIAndStream();
	};
	

	getById("gridlayout").appendChild(container);
	
	function handleUIAndStream() {
		if (session.roomid !== false) {
			if (session.roomid === "" && (!session.view || session.view === "")) {
			} else {
				log("ROOMID ENABLED");
				log("Update Mixer Event on REsize SET");
				getById("head3").classList.add("hidden");
				getById("head3a").classList.add("hidden");
				joinRoom(session.roomid);
			}
		} else {
			getById("head3").classList.remove("hidden");
			getById("head3a").classList.remove("hidden");
			getById("logoname").style.display = "none";
		}
		
		updatePushId();
		
		getById("head1").className = "hidden";
		getById("head2").className = "hidden";
		
		if (!session.cleanOutput) {
			getById("chatbutton").className = "float";
			getById("mediafileshare").classList.remove("hidden");
			getById("hangupbutton").className = "float";
			getById("controlButtons").classList.remove("hidden");
		} else {
			getById("controlButtons").classList.add("hidden");
		}

		toggleMute(true);
		
		if (session.director) {
		} else if (session.scene !== false) {
		} else if (session.roomid !== false) {
			if (session.roomid === "") {
				if (!session.view || session.view === "") {
					if (session.fullscreen) {
						session.windowed = session.windowed === null ? false : session.windowed;
					} else if (session.minipreview) {
						session.windowed = session.windowed === null ? false : session.windowed;
					} else {
						session.windowed = session.windowed === null ? true : session.windowed;
					}

					if (session.windowed) {
						session.videoElement.className = "myVideo clean fileshare";
						container.classList.add("vidcon");
					}
					getById("mutespeakerbutton").classList.add("hidden");
					container.style.width = "100%";
					container.style.alignItems = "center";
					container.backgroundColor = "#666";
					play();
				} else {
					session.windowed = session.windowed === null ? false : session.windowed;
					play();
				}
			} else {
				if (session.stereo == 5) {
					session.stereo = 3;
				}
				session.windowed = session.windowed === null ? false : session.windowed;
			}
		} else {
			if (session.fullscreen) {
				session.windowed = session.windowed === null ? false : session.windowed;
			} else if (session.minipreview) {
				session.windowed = session.windowed === null ? false : session.windowed;
			} else {
				session.windowed = session.windowed === null ? true : session.windowed;
			}
			if (session.windowed) {
				session.videoElement.className = "myVideo clean fileshare";
				container.classList.add("vidcon");
			}
			getById("mutespeakerbutton").classList.add("hidden");
			container.style.width = "100%";
			container.style.alignItems = "center";
			container.backgroundColor = "#666";
		}
		applyMirror(session.mirrorExclude);
		
		updateReshareLink();
		pokeIframeAPI("started-fileshare");
		pokeIframeAPI("file-share", true);

		clearInterval(session.updateLocalStatsInterval);
		session.updateLocalStatsInterval = setInterval(function () {
			updateLocalStats();
		}, session.statsInterval);
		
		if (session.whipOutput) {
			whipOut();
		}
		if (session.meshcast) {
			meshcast();
		}
		if (session.whepHost) {
			whepOut();
		}

		session.seeding = true;

		if (session.videoMutedFlag) {
			session.videoMuted = true;
			toggleVideoMute(true);
		}

		session.seedStream();
	}
}; // publishFile

class CanvasStreamSource {
    constructor() {
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d', { willReadFrequently: true });
        this.stream = this.canvas.captureStream(30);
		
        this.initialized = false;
		this.boundHandleFrame = this.handleFrame.bind(this);
        this.lastFrameTime = Date.now();
        this.indicatorRotation = 0;
        
        this.frameCheckInterval = setInterval(() => {
            this.drawKeyframeTrigger();
        }, 100);
		window.addEventListener('message', this.boundHandleFrame);
    }

    drawKeyframeTrigger() {
        if (!this.canvas || !this.ctx || Date.now() - this.lastFrameTime < 500) return;
        
        const state = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
        
        this.ctx.save();
        this.ctx.translate(this.canvas.width - 15, this.canvas.height - 15);
        this.ctx.rotate(this.indicatorRotation);
        
        const opacity = 0.15 + Math.sin(this.indicatorRotation) * 0.05;
        this.ctx.strokeStyle = `rgba(200, 200, 200, ${opacity})`;
        this.ctx.lineWidth = 1;
        
        const indicator = new Path2D();
        indicator.arc(0, 0, 3, 0, Math.PI * 2);
        indicator.moveTo(5, 0);
        indicator.arc(0, 0, 5, 0, Math.PI * 2);
        this.ctx.stroke(indicator);
        
        this.ctx.restore();
        this.ctx.putImageData(state, 0, 0);
        
        this.indicatorRotation += 0.01;
    }

    initializeForFrame(frame) {
        if (this.initialized) return;
        
        try {
            if (typeof frame !== "string") {
                const videoTrack = new MediaStreamTrackGenerator({ kind: 'video' });
                this.writer = videoTrack.writable.getWriter();
                const newStream = new MediaStream([videoTrack]);
                
                this.stream.getVideoTracks().forEach(track => {
                    this.stream.removeTrack(track);
                    track.stop();
                });
                newStream.getVideoTracks().forEach(track => {
                    this.stream.addTrack(track);
                });
            }
            this.initialized = true;
        } catch(e) {
            console.error("Failed to initialize MediaStreamTrackGenerator, keeping canvas:", e);
            this.initialized = true;
        }
    }

    handleFrame(event) {
        if (!event.data || !event.data.frame || (event.data.type != "canvas-frame")) return;
        
        const frame = event.data.frame;
        this.initializeForFrame(frame);
        if (this.writer && typeof frame !== "string") {
            this.writer.write(frame).catch(e => {
                console.error("Error writing frame:", e);
                if (frame.close) frame.close();
            });
        } else if (this.canvas) {
            const img = new Image();
            img.onload = () => {
                this.canvas.width = img.width;
                this.canvas.height = img.height;
                this.ctx.drawImage(img, 0, 0);
                this.lastFrameTime = Date.now();
                img.remove();
            };
            img.src = frame;
        }
    }

    getStream() {
        return this.stream;
    }

    destroy() {
        if (this.frameCheckInterval) {
            clearInterval(this.frameCheckInterval);
        }
        if (this.writer) {
            this.writer.close();
        }
        if (this.stream) {
            this.stream.getTracks().forEach(track => {
                track.stop();
                this.stream.removeTrack(track);
            });
        }
        if (this.imgSrc) {
            URL.revokeObjectURL(this.imgSrc);
        }
        window.removeEventListener('message', this.boundHandleFrame);
        this.canvas = null;
        this.ctx = null;
        this.stream = null;
        this.writer = null;
        this.initialized = false;
    }
}

session.publishFrameSource = function (ele, event) {
	
	var container = document.createElement("div");
	container.id = "container";

	if (session.cover) {
		container.style.setProperty("height", "100%", "important");
	}

	var v = createVideoElement();
	v.container = container;
	
	if (session.cleanOutput) {
		container.style.height = "100%";
		v.style.maxWidth = "100%";
		v.style.boxShadow = "none";
	}
	
	container.appendChild(v);

	if (session.streamID) {
		v.dataset.sid = session.streamID;
	}

	getById("gridlayout").appendChild(container);
	
	if (session.roomid !== false) {
		if (session.roomid === "" && (!session.view || session.view === "")) {
		} else {
			log("ROOMID EANBLED");
			log("Update Mixer Event on REsize SET");
			//window.addEventListener("resize", updateMixer);// TODO FIX
			//window.addEventListener("orientationchange", updateMixer);// TODO FIX
			getById("head3").classList.add("hidden");
			getById("head3a").classList.add("hidden");
			joinRoom(session.roomid);
		}
	} else {
		getById("head3").classList.remove("hidden");
		getById("head3a").classList.remove("hidden");
		getById("logoname").style.display = "none";
	}
	
	updatePushId();
	
	getById("head1").className = "hidden";
	getById("head2").className = "hidden";
	
	if (!session.cleanOutput) {
		getById("chatbutton").className = "float";
		getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though.
		getById("hangupbutton").className = "float";
		getById("controlButtons").classList.remove("hidden");
		//getById("helpbutton").style.display = "inherit";
		//getById("reportbutton").style.display = "";
	} else {
		getById("controlButtons").classList.add("hidden");
	}

	var bigPlayButton = document.getElementById("bigPlayButton");
	if (bigPlayButton) {
		bigPlayButton.parentNode.removeChild(bigPlayButton);
	}

	v.autoplay = false;
	if (session.showControls !== null) {
		v.controls = session.showControls;
	} else {
		v.controls = true;
	}
	v.muted = false;

	
	v.id = "videosource"; // could be set to UUID in the future
	v.dataset.menu = "context-menu-video";
	v.setAttribute("playsinline", "");
	
	session.canvasSource = new CanvasStreamSource();
	session.streamSrc = session.canvasSource.getStream();
	v.srcObject = session.streamSrc;
	
	v.className = "tile clean";
	
	session.videoElement = v;
	session.mirrorExclude = true;

	v.addEventListener("click", (e) => {
		log("click");
		try {
			if (e.ctrlKey || e.metaKey) {
				e.preventDefault();

				var [menu, innerMenu] = statsMenuCreator();

				menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);

				printMyStats(innerMenu);
				e.stopPropagation();
				return false;
			}
		} catch (e) {
			errorlog(e);
		}
	});

	v.touchTimeOut = null;
	v.touchLastTap = 0;
	v.touchCount = 0;
	
	v.addEventListener("touchend", (event) => {
		if (session.disableMouseEvents) {
			return;
		}
		log("touched");

		//document.ontouchup = null;
		//document.onmouseup = null;
		document.onmousemove = null;
		document.ontouchmove = null;

		var currentTime = new Date().getTime();
		var tapLength = currentTime - v.touchLastTap;
		clearTimeout(v.touchTimeOut);
		if (tapLength < 500 && tapLength > 0) {
			///
			log("double touched");
			v.touchCount += 1;
			event.preventDefault();
			if (v.touchCount < 5) {
				v.touchLastTap = currentTime;
				return false;
			}
			v.touchLastTap = 0;
			v.touchCount = 0;

			var [menu, innerMenu] = statsMenuCreator();

			menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);

			printMyStats(innerMenu);
			event.stopPropagation();
			return false;
			//////
		} else {
			v.touchCount = 1;
			v.touchTimeOut = setTimeout(
				function (vv) {
					clearTimeout(vv.touchTimeOut);
					vv.touchLastTap = 0;
					vv.touchCount = 0;
				},
				5000,
				v
			);
			v.touchLastTap = currentTime;
		}
	});

	v.onerror = (e) => {
		errorlog(e);
	};

	v.onloadeddata = async () => {
		
		session.mediafileShare = true;
		getById("mainmenu").remove();
		
		if (Firefox) {
			session.streamSrc = v.mozCaptureStream();
		} else {
			session.streamSrc = v.captureStream(); // gaaaaaaaaaaaahhhhhhhh!
		}
		
		toggleMute(true);
		
		if (session.director) {
		} else if (session.scene !== false) {
		} else if (session.roomid !== false) {
			if (session.roomid === "") {
				if (!session.view || session.view === "") {
					if (session.fullscreen) {
						session.windowed = session.windowed === null ? false : session.windowed;
					} else if (session.minipreview) {
						session.windowed = session.windowed === null ? false : session.windowed;
					} else {
						session.windowed = session.windowed === null ? true : session.windowed;
					}

					if (session.windowed) {
						v.className = "myVideo clean";
						container.classList.add("vidcon");
					}
					getById("mutespeakerbutton").classList.add("hidden");
					container.style.width = "100%";
					container.style.alignItems = "center";
					container.backgroundColor = "#666";
					play();
				} else {
					session.windowed = session.windowed === null ? false : session.windowed;
					play();
				}
			} else {
				//session.cbr=0; // we're just going to override it
				if (session.stereo == 5) {
					session.stereo = 3;
				}
				session.windowed = session.windowed === null ? false : session.windowed;
			}
			applyMirror(session.mirrorExclude);
		} else {
			if (session.fullscreen) {
				session.windowed = session.windowed === null ? false : session.windowed;
			} else if (session.minipreview) {
				session.windowed = session.windowed === null ? false : session.windowed;
			} else {
				session.windowed = session.windowed === null ? true : session.windowed;
			}
			if (session.windowed) {
				v.className = "myVideo clean ";
				container.classList.add("vidcon");
			}
			getById("mutespeakerbutton").classList.add("hidden");
			container.style.width = "100%";
			container.style.alignItems = "center";
			container.backgroundColor = "#666";
			applyMirror(session.mirrorExclude);
		}
		
		updateReshareLink();
		pokeIframeAPI("started-frameshare"); // depreciated
		pokeIframeAPI("frame-share", true);

		clearInterval(session.updateLocalStatsInterval);
		session.updateLocalStatsInterval = setInterval(function () {
			updateLocalStats();
		}, session.statsInterval);
		
		if (session.whipOutput){ // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024
			whipOut();
		}
		if (session.meshcast){
			await meshcast();
		}
		if (session.whepHost){
			whepOut();
		}

		session.seeding = true;

		if (session.videoMutedFlag) {
			session.videoMuted = true;
			toggleVideoMute(true);
		}

		session.seedStream();
	};
}; // publishFrameSource

function tryAgain(event) {
	// audio or video agnostic track reconnect ------------not actually in use,.  maybe out of date
	log("TRY AGAIN TRIGGERED");
	warnlog(event);
}

function enterPressedClick(event, ele) {
	if (event.keyCode === 13) {
		event.preventDefault();
		ele.click();
	}
}

function enterPressed(event, callback) {
	// Number 13 is the "Enter" key on the keyboard
	if (event.keyCode === 13) {
		event.preventDefault();
		callback();
	}
}

function dragElement(elmnt) {
	if (session.disableMouseEvents) {
		return;
	}
	log("dragElement started");

	function onvideoclick() {
		log("onvideoclick");
		log(pos3 + " " + pos4);
		//log(pos3o + " " + pos4o);
		tapToFocus(parseInt((pos3 * 100) / elmnt.clientWidth), parseInt((pos4 / elmnt.clientHeight) * 100));
		return false;
	}

	function elementDrag(e) {
		e = e || window.event;
		e.preventDefault();
		// calculate the new cursor position:
		log("dragging");
		log(e);
		if (Date.now() - millis < 100) {
			return;
		}

		dragged = true;
		millis = Date.now();

		if (e.type == "touchstart" || e.type == "touchmove" || e.type == "touchend" || e.type == "touchcancel") {
			var touch = e.touches[0] || e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
			pos1 = touch.clientX;
			pos2 = touch.clientY;
		} else if (e.type == "mousedown" || e.type == "mouseup" || e.type == "mousemove" || e.type == "mouseover" || e.type == "mouseout" || e.type == "mouseenter" || e.type == "mouseleave") {
			pos1 = e.clientX;
			pos2 = e.clientY;
		}

		if (!zoomable) {
			return;
		}

		var zoom = parseFloat(((pos4 - pos2) * 2) / elmnt.offsetHeight);

		if (zoom > 1) {
			zoom = 1.0;
		} else if (zoom < -1) {
			zoom = -1.0;
		}
		input.value = zoom * (input.max - input.min) + input.min;
		updateCameraConstraints("zoom", input.value, false, false);
	}
	function closeDragElement(e) {
		log("closeDragElement");
		log(e);

		// focusable
		if (!dragged) {
			log("dragged: " + dragged);
			onvideoclick();
		}
		dragged = false;

		elmnt.removeEventListener("touchend", closeDragElement);
		elmnt.removeEventListener("mouseup", closeDragElement);

		/* stop moving when mouse button is released:*/
		//document.ontouchend = null;
		//document.onmouseup = null;
		document.onmousemove = null;
		document.ontouchmove = null;
	}
	function dragMouseDown(e) {
		log("dragMouseDown");
		log(e);

		dragged = false;
		millis = Date.now();

		e = e || window.event;
		e.preventDefault();

		pos0 = input.value;
		if (e.type == "touchstart" || e.type == "touchmove" || e.type == "touchend" || e.type == "touchcancel") {
			var touch = e.touches[0] || e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
			pos3 = touch.clientX;
			pos4 = touch.clientY;
			//pos3o = touch.offsetX;
			//pos4o = touch.offsetX;
		} else if (e.type == "mousedown" || e.type == "mouseup" || e.type == "mousemove" || e.type == "mouseover" || e.type == "mouseout" || e.type == "mouseenter" || e.type == "mouseleave") {
			pos3 = e.clientX;
			pos4 = e.clientY;
			//pos3o = e.offsetX;
			//pos4o = e.offsetX;
		}
		elmnt.addEventListener("touchend", closeDragElement);
		elmnt.addEventListener("mouseup", closeDragElement);
		document.ontouchmove = elementDrag;
		document.onmousemove = elementDrag;
	}

	try {
		var stream = elmnt.srcObject;
		try {
			var track0 = stream.getVideoTracks();
		} catch (e) {
			return;
		}

		if (!track0.length) {
			return;
		}
		var focusable = false;
		var zoomable = false;
		var dragged = false;
		var input = getById("zoomSlider");
		track0 = track0[0];
		if (track0.getCapabilities) {
			var capabilities = track0.getCapabilities();
			var settings = track0.getSettings();

			if ("focusDistance" in capabilities) {
				log("focusable");
				focusable = true;
			}

			if ("zoom" in capabilities) {
				if (capabilities.zoom.min !== capabilities.zoom.max) {
					log("zoomable;");
					zoomable = true;
					input.min = capabilities.zoom.min;
					input.max = capabilities.zoom.max;
					input.step = capabilities.zoom.step;
					input.value = settings.zoom;
				}
			}
		}

		var millis = Date.now();
		var pos0 = 1;
		var pos3 = 0;
		var pos4 = 0;
		var pos1 = 0;
		var pos2 = 0;
		//var pos3o = 0;
		//var pos4o = 0;
	} catch (e) {
		errorlog(e);
		return;
	}

	if (!focusable && !zoomable) {
		return;
	} // can't be zoomed or focused.

	log("drag on");
	elmnt.onmousedown = dragMouseDown;
	elmnt.ontouchstart = dragMouseDown;
}

function previewIframe(iframeSrc) {
	// this is pretty important if you want to avoid camera permission popup problems.  You can also call it automatically via: <body onload=>loadIframe();"> , but don't call it before the page loads.
	if (!session.iFramesAllowed){
		warnUser("Can't create iFRAME - security is tainted due to possible CSS injection");
		errorlog("Can't create iFRAME - security is tainted due to possible CSS injection");
		return;
	}
	var iframe = document.createElement("iframe");
	iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location
	iframe.style.width = "100%";
	iframe.style.height = "100%";
	iframe.style.border = "10px dashed rgb(64 65 62)";
	iframe.classList.add("insecure");
	iframe.setAttribute("allowtransparency", "true");
	iframe.setAttribute("crossorigin", "anonymous");
	iframe.setAttribute("credentialless", "true");

	iframeSrc = parseURL4Iframe(iframeSrc);

	/* if (typeof iframeSrc == "object"){ // special handler.
		iframeSrc = iframeSrc.parsedSrc;
	}  */

	iframe.src = iframeSrc;
	getById("previewIframe").innerHTML = "";
	getById("previewIframe").style.width = "640px";
	getById("previewIframe").style.height = "360px";
	getById("previewIframe").style.margin = "auto";
	getById("previewIframe").appendChild(iframe);
}

function loadIframe(iframesrc, target) {
	// this is pretty important if you want to avoid camera permission popup problems.  You can also call it automatically via: <body onload=>loadIframe();"> , but don't call it before the page loads.
	/* if (document.getElementById("mainmenu")) {
		var m = getById("mainmenu");
		m.remove();
	} */
	
	if (!session.iFramesAllowed){
		return false;
	}
	
	if (!target){
		return false;
	}
	
	if (typeof target == "string"){
		let UUID = target;
		var iframe = document.createElement("iframe");
		iframe.style.width = "100%";
		iframe.style.height = "100%";
		iframe.id = "iframe_" + UUID;
		iframe.dataset.UUID = UUID;
		iframe.loadedYoutubeListen = false;
	
		if (session.director) {
			//
		} else if (session.scene !== false) {
			if (session.view) {
				// specific video to be played
				iframe.style.display = "block";
			} else if (session.scene === "0") {
				iframe.style.display = "block";
			} else {
				// group scene I guess; needs to be added manually
				iframe.style.display = "none";
			}
		} else if (session.roomid !== false) {
			//
		} else {
			iframe.style.display = "block";
		}
	} else {
		var iframe = target;
	}
	
	iframe.classList.add("insecure");
	iframe.setAttribute("allowtransparency", "true");
	iframe.setAttribute("crossorigin", "anonymous");
	iframe.setAttribute("credentialless", "true");
	iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location
	
	if (iframesrc == "") {
		iframesrc = "./";
		iframe.classList.remove("insecure");
	}

	// trusted domains
	var ipsafe = false;

	if (iframesrc.startsWith("https://www.youtube.com/") || iframesrc.startsWith("https://youtube.com/")) {
		iframe.classList.remove("insecure");
		setTimeout(
			function (iframe_id) {
				YoutubeListen(iframe_id);
			},
			1000,
			iframe.id
		); // create stats feedback for the director; syncing.
		if (session.noaudio) {
			if (iframesrc.includes("?")) {
				iframesrc += "&mute=1";
			} else {
				iframesrc += "?mute=1";
			}
		}
		ipsafe = true;
	} else if (iframesrc.includes("vdo.ninja/")) {
		iframe.classList.remove("insecure");
		ipsafe = false;
		if (isIFrame){
			console.warn("You're not allow to put this domain inside an iframe of an iframe.");
			return false;
		}
	} else if (iframesrc.includes("obs.ninja/")) {
		iframe.classList.remove("insecure");
		ipsafe = false;
		if (isIFrame){
			console.warn("You're not allow to put this domain inside an iframe of an iframe.");
			return false;
		}
	} else if (iframesrc.includes("versus.cam/")) {
		iframe.classList.remove("insecure");
		ipsafe = false;
		if (isIFrame){
			console.warn("You're not allow to put this domain inside an iframe of an iframe.");
			return false;
		}
	} else if (iframesrc.includes("invite.cam/")) {
		ipsafe = false;
		if (isIFrame){
			console.warn("You're not allow to put this domain inside an iframe of an iframe.");
			return false;
		}
	} else if (iframesrc.startsWith("https://player.twitch.tv/")) {
		iframe.classList.remove("insecure");
		ipsafe = true;
	} else if (iframesrc.startsWith("https://x.com/")) {
		iframe.classList.remove("insecure");
		ipsafe = false;
	} else if (iframesrc.startsWith("https://twitch.tv/")) {
		iframe.classList.remove("insecure");
		ipsafe = true;
	} else if (iframesrc.startsWith("https://caption.ninja/")) {
		iframe.classList.remove("insecure");
		ipsafe = true;
	} else if (iframesrc.startsWith("https://www.twitch.tv/")) {
		iframe.classList.remove("insecure");
		ipsafe = true;
	} else if (iframesrc.startsWith("https://vimeo.com/")) {
		iframe.classList.remove("insecure");
		ipsafe = true;
	} else if (iframesrc.startsWith("https://player.vimeo.com/")) {
		iframe.classList.remove("insecure");
		ipsafe = true;
	} else if (iframesrc.startsWith("https://meshcast.io/")) {
		iframe.classList.remove("insecure");
		try {
			if (document.domain.endsWith(".vdo.ninja")) {
				document.domain = "vdo.ninja";
			}
		} catch (e) {
			errorlog(e);
		}
		ipsafe = true;
	} else if (iframesrc.startsWith("https://app.stageten.tv/")) {
		iframe.classList.remove("insecure");
		ipsafe = true;
	} else if (iframesrc.startsWith("https://socialstream.ninja/")) {
		iframe.classList.remove("insecure");
		ipsafe = false;
	} else if (session.cleanOutput && window.obsstudio){
		iframe.classList.remove("insecure");
	}
	
	if (!ipsafe){
		iframe.title = "⚠️ This section is an iframe that may be of untrusted origin. Use caution.";
	}

	if (!ipsafe && (urlParams.has("privacy") || urlParams.has("private"))) {
		if (session.cleanOutput || window.obsstudio) {
			iframesrc = "./confirm.html?clean&url=" + encodeURI(iframesrc);
		} else {
			iframesrc = "./confirm.html?url=" + encodeURI(iframesrc);
		}
	} else if (!ipsafe && (typeof target == "object")){ // likely widget
		if (session.widgetwidth<=28){ // 28 for marcus
			iframe.classList.remove("insecure");
		} else if (isIFrame && (session.widgetwidth<=50)){
			iframe.classList.remove("insecure");
		} else {
			if (session.cleanOutput || window.obsstudio) {
				iframesrc = "./confirm.html?clean&url=" + encodeURI(iframesrc);
			} else {
				iframesrc = "./confirm.html?url=" + encodeURI(iframesrc);
			}
		}
	}


	if (isIFrame && ["invite.cam","invitecamera.com", "vdo.ninja", "versus.cam", "dev.versus.cam", "backup.vdo.ninja", "proxy.vdo.ninia", "proxy.obs.ninja", "insecure.vdo.ninja", "insecure.obs.ninja", "rtc.ninja"].includes(getParentHostname())){
		iframe.classList.add("insecure");
	}
	
	iframe.src = iframesrc;

	pokeIframeAPI("iframe-loaded", iframesrc);
	return iframe;
}

function dropDownButtonAction(ele) {
	var ele = getById("dropButton");
	if (ele) {
		ele.parentNode.removeChild(ele);
		//getById('container-5').classList.remove('hidden');
		//getById('container-8').classList.remove('hidden');
		//getById('container-6').classList.remove('hidden');
		document.querySelectorAll("div.column.card").forEach(child => {
			child.classList.remove("hidden");
		});
	}
}

function updateConstraintSliders() {
	log("updateConstraintSliders");
	if (session.roomid !== false && session.roomid !== "" && session.director !== true && session.forceMediaSettings == false) {
		if (session.controlRoomBitrate !== false) {
			listCameraSettings();
		}
		if (session.effect !== false) {
			//if ((iOS) || (iPad)){
			//} else {
			getById("effectsDiv3").style.display = "block";
			getById("effectSelector3").value = session.effect || "0";
			//}
		}
	} else {
		listAudioSettings();
		listCameraSettings();

		//if ((iOS) || (iPad)){
		//	} else {
		if (session.effect !== false) {
			getById("effectsDiv3").style.display = "block";
			try {
				getById("effectSelector3").value = session.effect || "0";
			} catch (E) {}
		}
		//}
	}
	//checkIfPIP();  //  this doesn't actually work on iOS still, so whatever.
}

function checkIfPIP() {
	try {
		if (session.videoElement && ((session.videoElement.webkitSupportsPresentationMode && typeof session.videoElement.webkitSetPresentationMode === "function") || document.pictureInPictureEnabled || !videoElement.disablePictureInPicture)) {
			// Toggle PiP when the user clicks the button.

			getById("pIpStartButton").addEventListener("click", function (event) {
				//	if ( (document.pictureInPictureEnabled || !videoElement.disablePictureInPicture)){
				//session.videoElement.requestPictureInPicture();
				//	} else {
				session.videoElement.webkitSetPresentationMode(session.videoElement.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture");
				//	}
			});
			getById("pIpStartButton").style.display = "inline-block";
		}
	} catch (e) {
		errorlog(e);
	}
}

function togglePictureInPicture(videoElement) {
	if (document.pictureInPictureElement) {
		if (document.pictureInPictureElement.id == videoElement.id) {
			document.exitPictureInPicture();
			pokeIframeAPI("picture-in-picture", false);
			return false;
		} else {
			document.exitPictureInPicture();
			pokeIframeAPI("picture-in-picture", false);
			videoElement.requestPictureInPicture();
			pokeIframeAPI("picture-in-picture", true);
		}
	} else if (document.pictureInPictureEnabled) {
		videoElement.requestPictureInPicture();
		pokeIframeAPI("picture-in-picture", true);
	}
	return true;
}

function mixMinusAudio(uid = false) {
	if (session.stereo === false) {
		var merger = session.audioCtx.createChannelMerger(1);
	} else {
		var merger = session.audioCtx.createChannelMerger(2);
	}

	if (session.videoElement && session.videoElement.srcObject) {
		var tracks = session.videoElement.srcObject.getAudioTracks();
		for (var i = 0; i < tracks.length; i++) {
			try {
				var tempStream = createMediaStream();
				tempStream.addTrack(tracks[i]);
				trackStream = session.audioCtx.createMediaStreamSource(tempStream);

				if (session.stereo !== false) {
					var splitter = session.audioCtx.createChannelSplitter(2);
					trackStream.connect(splitter);
					splitter.connect(merger, 0, 0);
					try {
						splitter.connect(merger, 1, 1);
					} catch (e) {
						errorlog(e);
						try {
							splitter.connect(merger, 0, 1); // hack.
						} catch (e) {
							errorlog(e);
						}
					}
				} else {
					trackStream.connect(merger, 0, 0);
				}
			} catch (e) {
				errorlog(e);
			}
		}
	}

	for (var UUID in session.rpcs) {
		if (uid && UUID === uid) {
			continue;
		}
		if (!session.rpcs[UUID].videoElement) {
			continue;
		} else if (!session.rpcs[UUID].videoElement.srcObject) {
			continue;
		}

		var tracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks();
		for (var i = 0; i < tracks.length; i++) {
			try {
				var tempStream = createMediaStream();
				tempStream.addTrack(tracks[i]);
				trackStream = session.audioCtx.createMediaStreamSource(tempStream);

				if (session.stereo !== false) {
					var splitter = session.audioCtx.createChannelSplitter(2);
					trackStream.connect(splitter);
					splitter.connect(merger, 0, 0);
					try {
						splitter.connect(merger, 1, 1);
					} catch (e) {
						errorlog(e);
						try {
							splitter.connect(merger, 0, 1); // hack.
						} catch (e) {
							errorlog(e);
						}
					}
				} else {
					trackStream.connect(merger, 0, 0);
				}
			} catch (e) {
				errorlog(e);
			}
		}
	}

	var destination = session.audioCtx.createMediaStreamDestination();
	merger.connect(destination);
	return destination.stream;
}

function listAudioSettingsPrep() {
	try {
		var tracks = session.streamSrc.getAudioTracks();
		if (!tracks.length) {
			warnlog("session.streamSrc contains no audio tracks");
			//return;
		}
	} catch (e) {
		warnlog(e);
		return;
	}

	var data = [];

	for (var i = 0; i < tracks.length; i += 1) {
		track0 = tracks[i];
		var trackSet = {};

		if (track0.getCapabilities) {
			trackSet.audioConstraints = track0.getCapabilities();
		} else if (Firefox) {
			// let's pretend like Firefox doesn't actually suck
			trackSet.audioConstraints = {
				autoGainControl: [true, false],
				//		"channelCount": {
				//			"max": 2,
				//			"min": 1
				//		},
				//		"deviceId": "default",
				echoCancellation: [true, false],
				//		"groupId": "a3cbdec54a9b6ed473fd950415626f7e76f9d1b90f8c768faab572175a355a17",
				//		"latency": {
				//			"max": 0.01,
				//			"min": 0.01
				//		},
				noiseSuppression: [true, false]
				//	"sampleRate": {
				//		"max": 48000,
				//		"min": 48000
				//	},
				//	"sampleSize": {
				//		"max": 16,
				//		"min": 16
				///	}
			};
		}

		if (track0.getSettings) {
			trackSet.currentAudioConstraints = track0.getSettings();
			if (!session.stereo) {
				try {
					delete trackSet.currentAudioConstraints.channelCount;
					delete trackSet.audioConstraints.channelCount;
				} catch (e) {}
			} else if (session.audioInputChannels && session.audioInputChannels == 1) {
				// this is pretty hacky, but it gets around not being able to actually set 1-channel. Not sure why.
				trackSet.currentAudioConstraints.channelCount = 1;
			}
		}

		trackSet.trackLabel = "unknown or none";
		if (track0.label) {
			trackSet.trackLabel = track0.label;
		}
		if (track0.id) {
			trackSet.deviceId = track0.id;
		}
		if (i == 0) {
			trackSet.equalizer = session.equalizer; // only supporting the first track at the moment.

			for (var waid in session.webAudios) {
				// TODO:  EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){..
				try {
					trackSet.lowEQ = session.webAudios[waid].lowEQ.gain.value;
					trackSet.midEQ = session.webAudios[waid].midEQ.gain.value;
					trackSet.highEQ = session.webAudios[waid].highEQ.gain.value;
				} catch (e) {}
				break;
			}
		} else {
			trackSet.equalizer = false;
		}

		if (i == 0) {
			trackSet.lowcut = session.lowcut; // only supporting the first track at the moment.
			if (session.lowcut) {
				for (var waid in session.webAudios) {
					// TODO:  EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){..
					try {
						trackSet.lowcut = session.webAudios[waid].lowcut1.frequency.value;
					} catch (e) {}
					break;
				}
			}
		} else {
			trackSet.lowcut = false;
		}

		trackSet.subGain = false;
		for (var waid in session.webAudios) {
			// TODO:  EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){..
			try {
				if (session.webAudios[waid].subGainNodes && track0.id in session.webAudios[waid].subGainNodes) {
					trackSet.subGain = session.webAudios[waid].subGainNodes[track0.id].gain.value;
				}
				break;
			} catch (e) {}
		}

		if (i == 0 && !session.disableWebAudio) {
			// if web audio is disabled, don't show them
			trackSet.gating = session.noisegate;
			trackSet.compressor = session.compressor;
			trackSet.micDelay = session.micDelay;
		}

		data.push(trackSet);
	}

	pokeIframeAPI("listing-audio-settings", data);
	return data;
}

function listVideoSettingsPrep() {
	try {
		var track0 = session.streamSrc.getVideoTracks();
		if (track0.length) {
			track0 = track0[0];
			if (track0.getCapabilities) {
				session.cameraConstraints = track0.getCapabilities();
			}
			log(session.cameraConstraints);
		}
	} catch (e) {
		warnlog(e);
		return;
	}

	try {
		if (track0.getSettings) {
			session.currentCameraConstraints = track0.getSettings();

			if (screen && screen.orientation && screen.orientation.type) {
				if (screen.orientation.type.includes("portrait")) {
					if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
						session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
					}
				}
			} else if (window.matchMedia("(orientation: portrait)").matches) {
				// legacy
				if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
					session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
				}
			}
		}
	} catch (e) {
		warnlog(e);
		return;
	}
	var msg = {};
	msg.trackLabel = "unknown or none";
	if (track0.label) {
		msg.trackLabel = track0.label;
	}
	msg.currentCameraConstraints = session.currentCameraConstraints;
	msg.cameraConstraints = session.cameraConstraints;

	pokeIframeAPI("listing-video-settings", msg);
	return msg;
}

var Final_transcript = "";
var Interim_transcript = "";
var Recognition = null;

if ("webkitSpeechRecognition" in window) {
	var SpeechRecognition = webkitSpeechRecognition;
} else if ("SpeechRecognition" in window) {
	var SpeechRecognition = window.SpeechRecognition;
} else {
	var SpeechRecognition = false;
}

var TranscriptionCounter = 0;
var retriesRecognition = 0;
var activeRecognition = false;
var timeoutRecognition = null;
function setupClosedCaptions() {
	if (activeRecognition) {
		return;
	}
	activeRecognition = true;

	log("CLOSED CAPTIONING SETUP");

	if (SpeechRecognition) {
		Recognition = new SpeechRecognition();

		Recognition.lang = session.transcript;

		Recognition.continuous = true;
		Recognition.interimResults = true;
		Recognition.maxAlternatives = 0;

		Recognition.onstart = function () {
			log("started transcription: " + Date.now());
			clearTimeout(timeoutRecognition);
			timeoutRecognition = setTimeout(function () {
				retriesRecognition = 0;
			}, 10000);
		};
		Recognition.onerror = function (event) {
			if (retriesRecognition <= 3) {
				console.error(event);
			}
			errorlog(event);
		};
		Recognition.onend = function (e) {
			warnlog(e);
			log("Stopped transcription " + Date.now());
			clearTimeout(timeoutRecognition);
			timeoutRecognition = setTimeout(function () {
				Recognition.start();
			}, parseInt(500 * retriesRecognition * retriesRecognition)); // restart it if it fails.
			retriesRecognition += 1;
			if (retriesRecognition == 3) {
				console.error("Captioning service is having a problem connecting");
			}
		};

		Recognition.onresult = function (event) {
			Interim_transcript = "";
			if (typeof event.results == "undefined") {
				log(event);
				return;
			}
			for (var i = event.resultIndex; i < event.results.length; ++i) {
				if (event.results[i].isFinal) {
					Final_transcript += event.results[i][0].transcript;
				} else {
					Interim_transcript += event.results[i][0].transcript;
				}
			}

			if (Final_transcript.length > 0) {
				log("FINAL:" + Final_transcript);
				try {
					var data = {};
					data.isFinal = true;
					data.transcript = Final_transcript;
					data.counter = TranscriptionCounter;
					session.sendMessage(data);
					TranscriptionCounter += 1;
					Final_transcript = "";
					Interim_transcript = "";
					pokeIframeAPI("transcription-text", Final_transcript);
				} catch (e) {
					errorlog(e);
				}
			} else {
				try {
					var data = {};
					data.isFinal = false;
					data.transcript = Interim_transcript;
					data.counter = TranscriptionCounter;
					session.sendMessage(data);
				} catch (e) {
					errorlog(e);
					Interim_transcript = "";
				}
			}
		};

		Recognition.start();
	} else if (!session.cleanOutput) {
		warnUser(getTranslation("speech-not-suppoted"), false, false);
	}
}
async function requestGoogleDriveRecord(ele, state = null, bitrate = null, event = null) {
    var UUID = ele.dataset.UUID || null;
    // Handle CTRL+click for selection
    if (event && (event.ctrlKey || event.metaKey)) {
        ele.classList.toggle("armed");
        ele.ariaPressed = ele.classList.contains("armed") ? "true" : "false";
        
        // Add callback only once for all armed buttons
        if (document.querySelectorAll('[data-action-type="recorder-google-drive-remote"].armed').length === 1 && 
            ele.classList.contains("armed")) {
            Callbacks.push([multiGdriveRecord]);
        }
        return;
    }
    // Single button normal operation
    if (!state && ele.classList.contains("pressed")) {
        var msg = {};
        msg.requestVideoRecord = false;
        msg.googleDriveRecord = false;
        msg.UUID = UUID;
        session.sendRequest(msg, msg.UUID);
        ele.classList.remove("pressed");
        ele.ariaPressed = "false";
    } else if (state == null || state) {
        if (!(session.gdrive && session.gdrive.accessToken)) {
            session.gdrive = setupGoogleDriveUploader();
            if (session.gdrive.promise) {
                log("AWAITING PROMISE");
                try {
                    // Make sure we're initialized before requesting a token
                    await session.gdrive.ensureInitialized();
                    session.gdrive.requestAccessToken();
                    await session.gdrive.promise;
                    console.log("Promise resolved with token");
                } catch (e) {
                    console.error("Error getting token:", e);
                    ele.classList.remove("armed");
                    return;
                }
            }
        }
        
        var filename = UUID;
        if (session.rpcs[UUID]) {
            filename = session.rpcs[UUID].label || session.rpcs[UUID].streamID || UUID;
        }
        filename = filename.replace(/[\W]+/g, "_");
        filename = filename.substring(0, 55);
        filename += "_" + Date.now().toString();
        if (SafariVersion) {
            filename += ".mp4";
        } else {
            filename += ".webm";
        }
        
        log("PROMISE DONE");
        var uploadLink = await session.gdrive.startResumableUpload(filename);
        
        var msg = {};
        msg.requestVideoRecord = true;
        msg.googleDriveRecord = uploadLink;
        msg.UUID = UUID;
        
        if (bitrate === null) {
            window.focus();
            let response = await promptRecordingOptions(getTranslation("what-bitrate-gdrive"));
            if (response) {
                msg.value = response.bitrate;
                msg.recordConfig = response;
                session.sendRequest(msg, msg.UUID);
                ele.classList.add("pressed");
                ele.ariaPressed = "true";
                ele.classList.remove("armed");
            } else {
                ele.classList.remove("armed");
                return;
            }
        } else {
            msg.value = bitrate;
            session.sendRequest(msg, msg.UUID);
            ele.classList.add("pressed");
            ele.ariaPressed = "true";
            ele.classList.remove("armed");
        }
        
        pokeIframeAPI("request-video-record", msg.requestVideoRecord, UUID);
    }
}
async function multiGdriveRecord() {
    const armedButtons = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"].armed');
    if (!armedButtons.length) return;
	
	armedButtons.forEach(button => {
		button.classList.remove("armed");
		button.ariaPressed = "false";
	});
    
    // Get recording settings once for all buttons
    window.focus();
    let response = await promptRecordingOptions(getTranslation("what-bitrate-gdrive"));
    if (!response) {
        return;
    }
    
    // Set up Google Drive authentication once
    if (!(session.gdrive && session.gdrive.accessToken)) {
        session.gdrive = setupGoogleDriveUploader();
        if (session.gdrive.promise) {
            try {
                await session.gdrive.promise;
            } catch (e) {
                // Auth failed, clean up armed buttons
                armedButtons.forEach(button => {
                    button.classList.remove("armed");
                    button.ariaPressed = "false";
                });
                return;
            }
        }
    }
    
    // Process each armed button with the same settings
    for (const button of armedButtons) {
        const UUID = button.dataset.UUID || null;
        
        // Generate unique filename for each recording
        const filename = ((session.rpcs[UUID] && (session.rpcs[UUID].label || session.rpcs[UUID].streamID)) || UUID)
            .replace(/[\W]+/g, "_")
            .substring(0, 55) + 
            "_" + Date.now().toString() + 
            (SafariVersion ? ".mp4" : ".webm");
            
        // Get upload link for each recording
        const uploadLink = await session.gdrive.startResumableUpload(filename);
        
        // Create message with shared settings
        const msg = {
            requestVideoRecord: true,
            googleDriveRecord: uploadLink,
            UUID: UUID,
            value: response.bitrate,
            recordConfig: response
        };
        
        // Send request and update button state
        session.sendRequest(msg, msg.UUID);
        button.classList.add("pressed");
        button.classList.remove("armed");
        button.ariaPressed = "true";
        
        pokeIframeAPI("request-video-record", true, UUID);
    }
}

async function requestVideoRecord(ele, state = null, bitrate = null) {
	var UUID = ele.dataset.UUID || null;

	if (!state && ele.classList.contains("pressed")) {
		var msg = {};
		msg.requestVideoRecord = false;
		msg.UUID = UUID;
		session.sendRequest(msg, msg.UUID);
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
	} else if (state == null || state) {
		var msg = {};
		msg.requestVideoRecord = true;
		msg.UUID = UUID;
		if (bitrate === null) {
			window.focus();
			let response = await promptRecordingOptions(getTranslation("what-bitrate"));
			if (response) {
				msg.value = response.bitrate;
				msg.recordConfig = response;
				session.sendRequest(msg, msg.UUID);
				ele.classList.add("pressed");
				ele.ariaPressed = "true"; // "btn-HL-green"
			} else {return;}
		} else {
			msg.value = bitrate;
			session.sendRequest(msg, msg.UUID);
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
		}
	}
	pokeIframeAPI("request-video-record", msg.requestVideoRecord, UUID);
}

function changeOrderDirector(value) {
	if (session.order == false) {
		session.order = 0;
	}
	session.order += parseInt(value) || 0;

	var elements = document.querySelectorAll('[data-action-type="order-value-director"]');
	//log(elements);
	if (elements[0]) {
		elements[0].innerText = parseInt(session.order) || 0;
	}

	var data = {};
	data = {};
	data.order = session.order;
	session.sendPeers(data);
	pokeIframeAPI("director-order", data.order);
}

function changeOrder(value, UUID) {
	var msg = {};
	msg.changeOrder = value;
	msg.UUID = UUID;
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("change-order", value, UUID);
}

function requestVideoHack(keyname, value, UUID, ctrl = false) {
	var msg = {};
	msg.requestVideoHack = true;
	msg.keyname = keyname;
	msg.value = value;
	msg.UUID = UUID;
	msg.ctrl = ctrl;
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("request-video-setting", { value: value, keyname: keyname, ctrl: ctrl }, UUID);
}

function requestAudioHack(keyname, value, UUID, deviceId = "default") {
	var msg = {};
	msg.requestAudioHack = true;
	msg.keyname = keyname;
	msg.value = value;
	msg.UUID = UUID;
	msg.deviceId = deviceId;
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("request-audio-setting", { value: value, keyname: keyname, deviceId: deviceId }, UUID);
}

function requestChangeEQ(keyname, value, UUID, track = 0) {
	var msg = {};
	msg.requestChangeEQ = true;
	msg.keyname = keyname;
	msg.value = value;
	msg.UUID = UUID;
	msg.track = track; // pointless atm
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("request-change-eq", { value: value, keyname: keyname, track: track }, UUID);
}

function requestChangeGating(keyname, value, UUID, track = 0) {
	var msg = {};
	msg.requestChangeGating = true;
	msg.keyname = keyname;
	msg.value = value;
	msg.UUID = UUID;
	msg.track = track; // pointless atm
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("request-change-gating", { value: value, keyname: keyname, track: track }, UUID);
}
function requestChangeCompressor(keyname, value, UUID, track = 0) {
	var msg = {};
	msg.requestChangeCompressor = true;
	msg.keyname = keyname;
	msg.value = value;
	msg.UUID = UUID;
	msg.track = track; // pointless atm
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("request-change-compressor", { value: value, keyname: keyname, track: track }, UUID);
}
function requestChangeMicDelay(value, UUID, track = 0) {
	var msg = {};
	msg.requestChangeMicDelay = true;
	msg.value = value;
	msg.UUID = UUID;
	msg.track = track; // pointless atm
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("request-change-mic-delay", { value: value, track: track }, UUID);
}

function requestChangeSubGain(value, UUID, deviceId) {
	var msg = {};
	msg.requestChangeSubGain = true;
	msg.value = value;
	msg.UUID = UUID;
	msg.deviceId = deviceId; // pointless atm
	log(msg);
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("request-sub-gain", { value: value, deviceId: deviceId }, UUID);
}

function requestChangeLowcut(value, UUID, track = 0) {
	var msg = {};
	msg.requestChangeLowcut = true;
	msg.value = value;
	msg.UUID = UUID;
	msg.track = track; // pointless atm
	session.sendRequest(msg, msg.UUID);
	pokeIframeAPI("request-low-cut", value, UUID);
}

function toggleSystemPip(vid) {
	try {
		if (vid.webkitSupportsPresentationMode && typeof vid.webkitSetPresentationMode === "function") {
			vid.webkitSetPresentationMode(vid.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture");
		} else {
			if (document.pictureInPictureElemen) {
				document.exitPictureInPicture();
				vid.requestPictureInPicture();
			} else {
				vid.requestPictureInPicture();
			}
		}
	} catch (e) {
		errorlog(e);
	}
}

function updateDirectorsAudio(dataN, UUID) {
	var audioEle = document.createElement("div");
	query("#container_" + UUID + " .advancedAudioSettings").innerHTML = "";
	
	if (query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').classList.contains("pressed")){
		query("#container_" + UUID + " .advancedAudioSettings").classList.remove("hidden");
	}
	//query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').classList.add("pressed");
	//query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').ariaPressed = "true";

	//log(dataN);
	if (!dataN.length) {
		
		var label = document.createElement("label");
		label.innerText = "No microphone selected";
		label.style.display = "block";
		label.id = "remoteAudioLabel_" + UUID;
		label.dataset.nomic = true;
		label.classList.add("settingsLabel");
		label.dataset.UUID = UUID;
		audioEle.appendChild(label);
		
		query('[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]').disabled = true;
		
		query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioEle);
		return;
	}
	
	query('[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]').disabled = false;

	for (var n = 0; n < dataN.length; n += 1) {
		var data = dataN[n];

		if (dataN.length == 1) {
			if (data.trackLabel) {
				var label = document.createElement("label");
				label.innerText = data.trackLabel;
				label.style.display = "block";
				label.id = "remoteAudioLabel_" + UUID;
				label.classList.add("settingsLabel");
				label.dataset.UUID = UUID;
				audioEle.appendChild(label);
			}
		}
		//if (n !== 0) {
		//var label = document.createElement("span");
		//label.innerText = "Coming Soon";
		//audioEle.appendChild(label);
		//	continue; // remove to more than one audio device (assuming other fixes are applied)
		//}

		if ("micDelay" in data && n == 0) {
			var label = document.createElement("label");
			var i = "micDelay";
			var div = document.createElement("div");
			label.id = "label_" + i + "_" + UUID;
			label.htmlFor = "constraints_" + i + "_" + UUID;

			var input = document.createElement("input");
			input.min = 0;
			input.max = 500;
			input.value = data.micDelay || 0;

			input.title = "Previously was: " + input.value;

			input.type = "range";
			input.dataset.keyname = i;
			//input.dataset.labelname =  "mic delay (ms):";
			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + " (ms):";

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.value = data.micDelay || 0;
			manualInput.className = "manualInput";
			manualInput.id = "constraints_manual_" + i + "_" + UUID;
			manualInput.dataset.UUID = UUID;
			manualInput.dataset.track = n;

			input.dataset.track = n;
			input.dataset.UUID = UUID;
			input.id = "constraints_" + i + "_" + UUID;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;
			input.style.margin = "2px 0px 5px";

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.onchange = function (e) {
				//e.target.title = e.target.value;
				getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.oninput = function (e) {
				getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				if (Date.now() - remoteSliderTimeout > 100) {
					remoteSliderTimeout = Date.now();
					requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
				}
			};

			audioEle.appendChild(div);
			div.appendChild(label);
			div.appendChild(manualInput);
			audioEle.appendChild(input);
		}

		if (data.lowcut !== false && n == 0) {
			var label = document.createElement("label");
			var i = "lowCut";
			label.id = "label_" + i + "_" + UUID;
			label.htmlFor = "constraints_" + i + "_" + UUID;

			var input = document.createElement("input");
			input.min = 50;
			input.max = 150;
			input.value = data.lowcut;

			input.title = "Previously was: " + input.value;

			input.type = "range";
			input.dataset.keyname = i;
			//input.dataset.labelname =  "low cut:";
			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.value = data.lowcut;
			manualInput.className = "manualInput";
			manualInput.id = "constraints_manual_" + i + "_" + UUID;
			manualInput.dataset.UUID = UUID;
			manualInput.dataset.track = n;

			input.dataset.track = n;
			input.dataset.UUID = UUID;
			input.id = "constraints_" + i + "_" + UUID;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;
			input.style.margin = "2px 0px 5px";

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.onchange = function (e) {
				//e.target.title = e.target.value;
				getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.oninput = function (e) {
				getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				if (Date.now() - remoteSliderTimeout > 100) {
					remoteSliderTimeout = Date.now();
					requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
				}
			};

			audioEle.appendChild(label);
			audioEle.appendChild(manualInput);
			audioEle.appendChild(input);
		}

		if (data.equalizer && n == 0) {
			var label = document.createElement("label");
			var i = "Low_EQ";
			//label.id = "label_" + i + "_"+UUID;
			label.htmlFor = "constraints_" + i + "_" + UUID;

			var input = document.createElement("input");
			input.min = -50;
			input.max = 50;
			input.value = data.lowEQ;
			input.title = "Previously was: " + input.value;
			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.labelname = "low EQ:";

			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.value = data.lowEQ;
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i + "_" + UUID;
			manualInput.dataset.UUID = UUID;
			manualInput.dataset.track = n;

			input.dataset.track = n;
			input.dataset.UUID = UUID;
			input.id = "constraints_" + i + "_" + UUID;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;
			input.style.margin = "2px 0px 5px";

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				if (Date.now() - remoteSliderTimeout > 100) {
					remoteSliderTimeout = Date.now();
					requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
				}
			};

			audioEle.appendChild(label);
			audioEle.appendChild(manualInput);
			audioEle.appendChild(input);

			var label = document.createElement("label");
			var i = "midEQ";
			//label.id = "label_" + i + "_"+UUID;
			label.htmlFor = "constraints_" + i + "_" + UUID;

			var input = document.createElement("input");
			input.min = -50;
			input.max = 50;
			input.value = data.midEQ;
			input.title = "Previously was: " + input.value;
			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.labelname = "mid EQ:";

			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.value = data.midEQ;
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i + "_" + UUID;
			manualInput.dataset.UUID = UUID;
			manualInput.dataset.track = n;

			input.dataset.track = n;
			input.dataset.UUID = UUID;
			input.id = "constraints_" + i + "_" + UUID;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;
			input.style.margin = "2px 0px 5px";

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				if (Date.now() - remoteSliderTimeout > 100) {
					remoteSliderTimeout = Date.now();
					requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
				}
			};

			audioEle.appendChild(label);
			audioEle.appendChild(manualInput);
			audioEle.appendChild(input);

			var label = document.createElement("label");
			var i = "highEQ";
			//label.id = "label_" + i + "_"+UUID;
			label.htmlFor = "constraints_" + i + "_" + UUID;

			var input = document.createElement("input");
			input.min = -50;
			input.max = 50;
			input.value = data.highEQ;
			input.title = "Previously was: " + input.value;
			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.labelname = "high EQ:";

			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.value = data.highEQ;
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i + "_" + UUID;
			manualInput.dataset.UUID = UUID;
			manualInput.dataset.track = n;

			input.dataset.track = n;
			input.dataset.UUID = UUID;
			input.id = "constraints_" + i + "_" + UUID;
			input.classList.add("inputConstraint");
			input.name = "constraints_" + i;

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				if (Date.now() - remoteSliderTimeout > 100) {
					remoteSliderTimeout = Date.now();
					requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
				}
			};

			audioEle.appendChild(label);
			audioEle.appendChild(manualInput);
			audioEle.appendChild(input);
		}

		if ("gating" in data && n == 0) {
			// only show once.
			var label = document.createElement("label");
			var i = "noiseGate";
			var div = document.createElement("div");
			var label = document.createElement("label");
			label.id = "label_" + i + "_" + n + "_" + UUID;
			label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID;
			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
			label.style = "display:inline-block; padding:0;";
			label.dataset.keyname = i;
			label.dataset.track = n;
			var input = document.createElement("select");
			var c = document.createElement("option");

			var opt = new Option("Off", false);
			input.options.add(opt);
			opt = new Option("On", true);
			input.options.add(opt);

			if (data.gating) {
				opt.selected = "true";
			}

			input.dataset.deviceId = data.deviceId;
			input.id = "constraints_" + i + "_" + n + "_" + UUID;
			input.className = "constraintCameraInput";
			input.name = "constraints_" + i + "_" + n;
			input.style = "display:inline; padding:2px;";
			input.dataset.keyname = i;
			input.dataset.track = n;
			input.dataset.UUID = UUID;
			input.dataset.chosen = input.value;
			input.onchange = function (e) {
				this.dataset.chosen = this.value;
				//getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value;
				requestChangeGating("gating", e.target.value, e.target.dataset.UUID, parseInt(e.target.dataset.track));
				log(e.target.dataset.keyname, e.target.value);
			};
			audioEle.appendChild(div);
			div.appendChild(label);
			div.appendChild(input);
		}

		if ("compressor" in data && n == 0) {
			var label = document.createElement("label");
			var i = "compressor";
			var div = document.createElement("div");
			var label = document.createElement("label");
			label.id = "label_" + i + "_" + n + "_" + UUID;
			label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID;
			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
			label.style = "display:inline-block; padding:0;";
			label.dataset.keyname = i;
			label.dataset.track = n;

			var input = document.createElement("select");
			var c = document.createElement("option");

			var opt = new Option("Off", false);
			input.options.add(opt);
			opt = new Option("On", 1);
			input.options.add(opt);

			if (data.compressor == 1) {
				opt.selected = "true";
			}
			opt = new Option("Limiter", 2);
			input.options.add(opt);

			if (data.compressor == 2) {
				opt.selected = "true";
			}

			input.dataset.deviceId = data.deviceId;
			input.id = "constraints_" + i + "_" + n + "_" + UUID;
			input.className = "constraintCameraInput";
			input.name = "constraints_" + i + "_" + n;
			input.style = "display:inline; padding:2px;";
			input.dataset.keyname = i;
			input.dataset.track = n;
			input.dataset.UUID = UUID;
			input.dataset.chosen = input.value;
			input.onchange = function (e) {
				this.dataset.chosen = this.value;
				//getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value;
				requestChangeCompressor("compressor", e.target.value, e.target.dataset.UUID, parseInt(e.target.dataset.track));
				log(e.target.dataset.keyname, e.target.value);
			};
			audioEle.appendChild(div);
			div.appendChild(label);
			div.appendChild(input);
		}

		if (dataN.length > 1) {
			if (data.trackLabel) {
				var label = document.createElement("label");
				label.innerText = data.trackLabel;
				label.style.display = "block";
				label.id = "remoteAudioLabel_" + UUID + "_" + n + "_" + UUID;
				label.classList.add("settingsLabel");
				audioEle.appendChild(label);
			}
		}

		for (var i in data.audioConstraints) {
			try {
				log(i);
				log(data.audioConstraints[i]);
				if (typeof data.audioConstraints[i] === "object" && data.audioConstraints[i] !== null && "max" in data.audioConstraints[i] && "min" in data.audioConstraints[i]) {
					if (i === "aspectRatio") {
						continue;
					} else if (i === "width") {
						continue;
					} else if (i === "height") {
						continue;
					} else if (i === "frameRate") {
						continue;
					} else if (i === "latency") {
						//	continue;
					} else if (i === "sampleRate") {
						continue;
					} else if (i === "channelCount") {
						//	continue;
					} else if (i === "volume") {
						continue;
					}

					if (!("deviceId" in data.audioConstraints)) {
						continue;
					} // not going to support older versions.

					var label = document.createElement("label");
					//label.id = "label_" + i + "_"+n+ "_"+UUID;
					label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID;
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";

					var input = document.createElement("input");
					input.min = data.audioConstraints[i].min;
					input.max = data.audioConstraints[i].max;

					if (parseFloat(input.min) == parseFloat(input.max)) {
						continue;
					}

					var manualInput = document.createElement("input");
					manualInput.type = "number";

					if ("step" in data.audioConstraints[i]) {
						input.step = data.audioConstraints[i].step;
						manualInput.step = data.audioConstraints[i].step;
					} else if ("volume" == i) {
						input.step = 0.01;
						manualInput.step = 0.01;
					}

					manualInput.dataset.keyname = i;
					manualInput.className = "manualInput";
					manualInput.id = "label_" + i + "_" + n + "_" + UUID;
					manualInput.max = data.audioConstraints[i].max;
					manualInput.min = data.audioConstraints[i].min;
					manualInput.dataset.UUID = UUID;
					manualInput.dataset.track = n;
					manualInput.dataset.keyname = i;

					if (i in data.currentAudioConstraints) {
						input.value = data.currentAudioConstraints[i];
						manualInput.value = parseFloat(input.value);
						//label.innerText = i + ": " + data.currentAudioConstraints[i];
						label.title = "Previously was:  " + data.currentAudioConstraints[i];
						input.title = "Previously was:  " + data.currentAudioConstraints[i];
					} else {
						label.innerText = i;
					}

					if (i === "height" || i === "width") {
						input.title = "Hold CTRL (or cmd) to lock width and height together when changing them";
						input.min = 16;
					}

					input.type = "range";
					input.dataset.keyname = i;
					input.dataset.track = n;
					input.dataset.deviceId = data.deviceId;
					input.dataset.UUID = UUID;
					input.id = "constraints_" + i + "_" + n + "_" + UUID;
					input.style = "display:block; width:100%;";
					input.name = "constraints_" + i + "_" + n + "_" + UUID;

					if (i == "channelCount") {
						input.style.display = "none";
						manualInput.style.margin = "5px 0px 9px 10px";
					}

					manualInput.onchange = function (e) {
						getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
						requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId);
					};

					input.onchange = function (e) {
						//e.target.title = e.target.value;
						getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
						requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId);
					};

					audioEle.appendChild(label);
					audioEle.appendChild(manualInput);
					audioEle.appendChild(input);
				} else if (typeof data.audioConstraints[i] === "object" && data.audioConstraints[i] !== null) {
					if (i == "resizeMode") {
						continue;
					}

					var div = document.createElement("div");
					var label = document.createElement("label");
					label.id = "label_" + i + "_" + n + "_" + UUID;
					label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID;
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
					label.style = "display:inline-block; padding:0;";
					var input = document.createElement("select");
					var c = document.createElement("option");

					if (data.audioConstraints[i].length > 1) {
						for (var opts in data.audioConstraints[i]) {
							log(opts);
							if (data.audioConstraints[i][opts] === false) {
								var opt = new Option("Off", data.audioConstraints[i][opts]);
							} else if (data.audioConstraints[i][opts] === true) {
								var opt = new Option("On", data.audioConstraints[i][opts]);
							} else {
								var opt = new Option(data.audioConstraints[i][opts], data.audioConstraints[i][opts]);
							}
							input.options.add(opt);
							if (i in data.currentAudioConstraints) {
								if (data.audioConstraints[i][opts] == data.currentAudioConstraints[i]) {
									opt.selected = "true";
								}
							}
						}
					} else if (i.toLowerCase() == "torch") {
						var opt = new Option("Off", false);
						input.options.add(opt);
						opt = new Option("On", true);
						input.options.add(opt);
						try {
							if (i in data.currentAudioConstraints) {
								if (data.audioConstraints[i]["torch"] == true) {
									opt.selected = "true";
								}
							}
						} catch (e) {}
					} else {
						continue;
					}

					input.id = "constraints_" + i + "_" + n + "_" + UUID;
					input.className = "constraintCameraInput";
					input.name = input.id;
					input.style = "display:inline; padding:2px;";
					input.dataset.keyname = i;
					input.dataset.track = n;
					input.dataset.deviceId = data.deviceId;
					input.dataset.UUID = UUID;
					input.dataset.chosen = input.value;
					input.onchange = function (e) {
						this.dataset.chosen = this.value;
						//getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value;
						requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId);
						log(e.target.dataset.keyname, e.target.value);
					};
					audioEle.appendChild(div);
					div.appendChild(label);
					div.appendChild(input);
				} else if (typeof data.audioConstraints[i] === "boolean") {
					var div = document.createElement("div");
					var label = document.createElement("label");
					label.id = "label_" + i + "_" + n + "_" + UUID;
					label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID;
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
					label.style = "display:inline-block; padding:0;";
					label.dataset.keyname = i;
					label.dataset.track = n;
					var input = document.createElement("select");
					var c = document.createElement("option");

					var opt = new Option("Off", false);
					input.options.add(opt);
					opt = new Option("On", true);
					input.options.add(opt);

					try {
						if (data.audioConstraints[i] === true) {
							opt.selected = "true";
						}
					} catch (e) {}

					input.dataset.deviceId = data.deviceId;
					input.id = "constraints_" + i + "_" + n + "_" + UUID;
					input.className = "constraintCameraInput";
					input.name = input.id;
					input.style = "display:inline; padding:2px;";
					input.dataset.keyname = i;
					input.dataset.track = n;
					input.dataset.UUID = UUID;
					input.dataset.chosen = input.value;
					input.onchange = function (e) {
						this.dataset.chosen = this.value;
						//getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value;
						requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId);
						log(e.target.dataset.keyname, e.target.value);
					};
					audioEle.appendChild(div);
					div.appendChild(label);
					div.appendChild(input);
				}
			} catch (e) {
				errorlog(e);
			}
		}

		if (data.subGain !== false) {
			var label = document.createElement("label");
			var i = "Gain";
			var div = document.createElement("div");
			label.id = "label_" + i + "_" + n + "_" + UUID;
			label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID;

			var input = document.createElement("input");
			input.min = 0;
			input.max = 200;
			input.value = data.subGain * 100;
			input.title = "Previously was: " + parseInt(input.value);
			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.track = n;
			input.dataset.labelname = "Gain:";
			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.value = data.subGain * 100;
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i + "_" + n + "_" + UUID;
			manualInput.dataset.UUID = UUID;
			manualInput.dataset.track = n;

			input.dataset.track = data.deviceId;
			input.dataset.UUID = UUID;
			input.id = "constraints_" + i + "_" + n + "_" + UUID;
			input.style = "display:block; width:100%;";
			input.name = input.id;
			input.style.margin = "2px 0px 5px";

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track);
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track);
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
				if (Date.now() - remoteSliderTimeout > 100) {
					remoteSliderTimeout = Date.now();
					requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track);
				}
			};

			audioEle.appendChild(div);
			div.appendChild(label);
			div.appendChild(manualInput);
			audioEle.appendChild(input);
		}

		query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioEle);
	}

	if (fixScrollReset) {
		clearTimeout(fixScrollReset);
		fixScrollReset = null;
		getById("directorlayout").scrollTop = fixScrollResetValue;
	}
}

var remoteSliderTimeout = 0;

function updateDirectorsVideo(data, UUID) {
	var videoEle = document.createElement("div");
	if (data.trackLabel) {
		var label = document.createElement("label");
		label.innerText = data.trackLabel;
		label.style.display = "block";
		label.id = "remoteVideoLabel_" + UUID;
		label.dataset.UUID = UUID;
		label.classList.add("settingsLabel");
		videoEle.appendChild(label);
	}

	for (var i in data.cameraConstraints) {
		try {
			log(i);
			log(data.cameraConstraints[i]);

			if (i === "focusMode") {
				continue; // I'll handle this with FocusDistance instead
			} else if (i === "whiteBalanceMode") {
				continue; // I'll handle this elsewhere
			} else if (i === "exposureMode") {
				continue; // I'll handle this elsewhere
			}

			if (typeof data.cameraConstraints[i] === "object" && data.cameraConstraints[i] !== null && "max" in data.cameraConstraints[i] && "min" in data.cameraConstraints[i]) {
				if (i === "aspectRatio") {
					//	continue;
				} else if (i === "width") {
					//	continue;
				} else if (i === "height") {
					//	continue;
				} else if (i === "frameRate") {
					//	continue;
				} else if (i === "latency") {
					//	continue;
				} else if (i === "sampleRate") {
					continue;
				} else if (i === "channelCount") {
					//	continue;
				}

				var manualMode = false;
				var manualLabel = false;
				if (i === "exposureTime") {
					if (data.currentCameraConstraints["exposureMode"]) {
						manualMode = document.createElement("input");
						manualMode.type = "checkbox";
						manualMode.id = "manual_" + i + "_" + UUID;
						manualMode.dataset.UUID = UUID;
						manualMode.dataset.keyname = "exposureMode";
						manualMode.onchange = function (e) {
							var value = "manual";
							if (e.target.checked) {
								value = "continuous";
							}
							requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true);
							//getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
							//getById("label_" + e.target.dataset.keyname+ "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
						};

						manualLabel = document.createElement("label");
						manualLabel.htmlFor = manualMode.id;
						manualLabel.innerHTML = "Auto: ";
						manualLabel.style.marginLeft = "20px";
						if (data.currentCameraConstraints["exposureMode"] == "continuous") {
							manualMode.checked = true;
						}
					}
				} else if (i === "focusDistance") {
					if (data.currentCameraConstraints["focusMode"]) {
						manualMode = document.createElement("input");
						manualMode.type = "checkbox";
						manualMode.id = "manual_" + i + "_" + UUID;
						manualMode.dataset.UUID = UUID;
						manualMode.dataset.keyname = "focusMode";
						manualMode.onchange = function (e) {
							var value = "manual";
							if (e.target.checked) {
								value = "continuous";
							}
							requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true);
						};
						manualLabel = document.createElement("label");
						manualLabel.htmlFor = manualMode.id;
						manualLabel.innerHTML = "Auto: ";
						manualLabel.style.marginLeft = "20px";
						if (data.currentCameraConstraints["focusMode"] == "continuous") {
							manualMode.checked = true;
						}
					}
				} else if (i === "colorTemperature") {
					if (data.currentCameraConstraints["whiteBalanceMode"]) {
						manualMode = document.createElement("input");
						manualMode.type = "checkbox";
						manualMode.id = "manual_" + i + "_" + UUID;
						manualMode.dataset.UUID = UUID;
						manualMode.dataset.keyname = "whiteBalanceMode";
						manualMode.onchange = function (e) {
							var value = "manual";
							if (e.target.checked) {
								value = "continuous";
							}
							requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true);
						};
						manualLabel = document.createElement("label");
						manualLabel.htmlFor = manualMode.id;
						manualLabel.innerHTML = "Auto: ";
						manualLabel.style.marginLeft = "20px";
						if (data.currentCameraConstraints["whiteBalanceMode"] == "continuous") {
							manualMode.checked = true;
						}
					}
				}

				var label = document.createElement("label");
				//label.id = "label_" + i;
				label.htmlFor = "constraints_" + i + " _" + UUID;
				if (i === "colorTemperature") {
					label.innerText = "Color Temp:";
				} else if (i === "exposureCompensation") {
					label.innerText = "Exposure Comp:";
				} else if (i === "exposureTime") {
					label.innerText = "Exposure:";
				} else if (i === "focusDistance") {
					label.innerText = "Focus:";
				} else {
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
				}

				if (i === "zoom" || i === "pan" || i === "til") {
					label.innerHTML = "<span style='cursor:help;' title='Remote PTZ controls only work if the guest`s window is visible and not minimized'>⚠</span> " + label.innerText;
				}

				var input = document.createElement("input");

				if (i === "aspectRatio") {
					input.max = 5;
					input.min = 0.2;
					input.step = 0.00001;
				} else {
					input.min = data.cameraConstraints[i].min;
					input.max = data.cameraConstraints[i].max;
				}

				if (parseFloat(input.min) == parseFloat(input.max)) {
					continue;
				}

				if (i in data.currentCameraConstraints) {
					input.value = data.currentCameraConstraints[i];
					label.title = "Previously was:  " + data.currentCameraConstraints[i];
					input.title = "Previously was:  " + data.currentCameraConstraints[i];
				}

				input.type = "range";
				input.dataset.keyname = i;
				input.dataset.UUID = UUID;
				input.id = "constraints_" + i + "_" + UUID;
				input.name = input.id;
				input.classList.add("inputConstraint");
				input.manualMode = manualMode;

				if (i === "height" || i === "width") {
					input.title = "Hold CTRL (or cmd) to lock width and height together when changing them";
					input.min = 16;
				}

				var manualInput = document.createElement("input");
				manualInput.type = "number";
				manualInput.value = parseFloat(input.value);
				manualInput.className = "manualInput";
				manualInput.id = "label_" + i + "_" + UUID;
				manualInput.name = manualInput.id;
				manualInput.dataset.keyname = i;
				manualInput.dataset.UUID = UUID;
				manualInput.manualMode = manualMode;

				if ("step" in data.cameraConstraints[i]) {
					manualInput.step = data.cameraConstraints[i].step;
					input.step = data.cameraConstraints[i].step;
				} else if (i === "aspectRatio") {
					input.step = 0.000001;
					manualInput.step = 0.005;
				}

				manualInput.onchange = function (e) {
					getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
					if (e.target.manualMode) {
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true);
					} else {
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false);
					}
				};

				input.onchange = function (e) {
					getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
					//updateVideoConstraints(e.target.dataset.keyname, e.target.value);
					if (CtrlPressed || e.target.manualMode) {
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true);
					} else {
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false);
					}
				};

				input.oninput = function (e) {
					getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
					if (Date.now() - remoteSliderTimeout > 100) {
						remoteSliderTimeout = Date.now();
						if (CtrlPressed) {
							requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true);
						} else {
							requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false);
						}
					}
				};

				videoEle.appendChild(label);
				videoEle.appendChild(manualInput);

				if (manualMode && manualLabel) {
					videoEle.appendChild(manualLabel);
					videoEle.appendChild(manualMode);
				}

				if (i === "aspectRatio") {
					var preSelectButton = document.createElement("button");
					preSelectButton.value = 16 / 9.0;
					preSelectButton.innerText = "16:9";
					preSelectButton.dataset.keyname = i;
					preSelectButton.dataset.UUID = UUID;
					preSelectButton.className = "preSelectButton";
					preSelectButton.onclick = function (e) {
						getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
						getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false);
					};
					videoEle.appendChild(preSelectButton);
					var preSelectButton = document.createElement("button");
					preSelectButton.value = 9 / 16.0;
					preSelectButton.innerText = "9:16";
					preSelectButton.dataset.UUID = UUID;
					preSelectButton.className = "preSelectButton";
					preSelectButton.dataset.keyname = i;
					preSelectButton.onclick = function (e) {
						getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
						getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false);
					};
					videoEle.appendChild(preSelectButton);
				}

				videoEle.appendChild(input);
			} else if (typeof data.cameraConstraints[i] === "object" && data.cameraConstraints[i] !== null) {
				if (i == "resizeMode") {
					continue;
				}

				var div = document.createElement("div");
				var label = document.createElement("label");
				label.id = "label_" + i + "_" + UUID;
				label.name = label.id;
				label.htmlFor = "constraints_" + i + "_" + UUID;
				label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
				label.style = "display:inline-block; padding:0;";
				label.dataset.keyname = i;
				label.dataset.UUID = UUID;
				var input = document.createElement("select");
				var c = document.createElement("option");

				if (data.cameraConstraints[i].length > 1) {
					for (var opts in data.cameraConstraints[i]) {
						log(opts);

						if (data.cameraConstraints[i][opts] === false) {
							var opt = new Option("Off", data.cameraConstraints[i][opts]);
						} else if (data.cameraConstraints[i][opts] === true) {
							var opt = new Option("On", data.cameraConstraints[i][opts]);
						} else {
							var opt = new Option(data.cameraConstraints[i][opts], data.cameraConstraints[i][opts]);
						}

						input.options.add(opt);
						if (i in data.currentCameraConstraints) {
							if (data.cameraConstraints[i][opts] == data.currentCameraConstraints[i]) {
								opt.selected = "true";
							}
						}
					}
				} else if (i.toLowerCase() == "torch") {
					var opt = new Option("Off", false);
					input.options.add(opt);
					opt = new Option("On", true);
					input.options.add(opt);
					try {
						if (i in data.currentCameraConstraints) {
							if (data.cameraConstraints[i]["torch"] == true) {
								opt.selected = "true";
							}
						}
					} catch (e) {}
				} else {
					continue;
				}

				input.id = "constraints_" + i + "_" + UUID;
				input.className = "constraintCameraInput";
				input.name = input.id;
				input.dataset.UUID = UUID;
				input.style = "display:inline; padding:2px;";
				input.dataset.keyname = i;
				input.dataset.chosen = input.value;
				input.onchange = function (e) {
					this.dataset.chosen = this.value;
					//getById("label_"+e.target.dataset.keyname+ "_" + e.target.dataset.UUID).innerText =e.target.dataset.keyname+": "+e.target.value;
					//updateVideoConstraints(e.target.dataset.keyname, e.target.value);
					if (CtrlPressed) {
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true);
					} else {
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false);
					}
					log(e.target.dataset.keyname, e.target.value);
				};
				videoEle.appendChild(div);
				div.appendChild(label);
				div.appendChild(input);
			} else if (typeof data.cameraConstraints[i] === "boolean") {
				var div = document.createElement("div");
				var label = document.createElement("label");
				label.id = "label_" + i + "_" + UUID;
				label.name = label.id;
				label.htmlFor = "constraints_" + i + "_" + UUID;
				label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
				label.style = "display:inline-block; padding:0;";
				label.dataset.keyname = i;
				label.dataset.UUID = UUID;
				var input = document.createElement("select");
				var c = document.createElement("option");

				var opt = new Option("Off", false);
				input.options.add(opt);
				opt = new Option("On", true);
				input.options.add(opt);

				try {
					if (data.audioConstraints[i] === true) {
						opt.selected = "true";
					}
				} catch (e) {}

				input.id = "constraints_" + i + "_" + UUID;
				input.className = "constraintCameraInput";
				input.name = input.id;
				input.style = "display:inline; padding:2px;";
				input.dataset.UUID = UUID;
				input.dataset.keyname = i;
				input.dataset.chosen = input.value;
				input.onchange = function (e) {
					this.dataset.chosen = this.value;
					//getById("label_"+e.target.dataset.keyname+ "_" + e.target.dataset.UUID).innerText =e.target.dataset.keyname+": "+e.target.value;
					//updateVideoConstraints(e.target.dataset.keyname, e.target.value);
					if (CtrlPressed) {
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true);
					} else {
						requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false);
					}
					log(e.target.dataset.keyname, e.target.value);
				};
				videoEle.appendChild(div);
				div.appendChild(label);
				div.appendChild(input);
			}
		} catch (e) {
			errorlog(e);
		}
	}

	query("#container_" + UUID + " .advancedVideoSettings").innerHTML = "";
	query("#container_" + UUID + " .advancedVideoSettings").appendChild(videoEle);
	query("#container_" + UUID + " .advancedVideoSettings").classList.remove("hidden");

	if (fixScrollReset) {
		clearTimeout(fixScrollReset);
		fixScrollReset = null;
		getById("directorlayout").scrollTop = fixScrollResetValue;
	}
}

///////
function capitalizeFirstLetter(string) {
	return string.charAt(0).toUpperCase() + string.slice(1);
}

function listAudioSettings() {
	getById("popupSelector_constraints_audio").innerHTML = "";

	var tracks = session.streamSrc.getAudioTracks();
	if (!tracks.length) {
		warnlog("session.streamSrc contains no audio tracks");
		return;
	}

	for (var ii = 0; ii < tracks.length; ii++) {
		track0 = tracks[ii];
		if (track0.getCapabilities) {
			session.audioConstraints = track0.getCapabilities();
		} else if (Firefox) {
			// let's pretend like Firefox doesn't actually suck
			session.audioConstraints = {
				autoGainControl: [true, false],
				//		"channelCount": {
				//			"max": 2,
				//			"min": 1
				//		},
				//		"deviceId": "default",
				echoCancellation: [true, false],
				//		"groupId": "a3cbdec54a9b6ed473fd950415626f7e76f9d1b90f8c768faab572175a355a17",
				//		"latency": {
				//			"max": 0.01,
				//			"min": 0.01
				//		},
				noiseSuppression: [true, false]
				//	"sampleRate": {
				//		"max": 48000,
				//		"min": 48000
				//	},
				//	"sampleSize": {
				//		"max": 16,
				//		"min": 16
				///	}
			};
		}

		try {
			if (track0.getSettings) {
				session.currentAudioConstraints = track0.getSettings();

				if (!session.stereo) {
					try {
						delete session.currentAudioConstraints.channelCount;
						delete session.audioConstraints.channelCount;
					} catch (e) {}
				} else if (session.audioInputChannels && session.audioInputChannels == 1) {
					// this is pretty hacky, but it gets around not being able to actually set 1-channel. Not sure why.
					session.currentAudioConstraints.channelCount = 1;
				}
			}
		} catch (e) {
			errorlog(e);
		}

		//////
		if (ii == 0) {
			for (var webAudio in session.webAudios) {
				if (session.webAudios[webAudio].gainNode) {
					if (getById("popupSelector_constraints_audio").style.display == "none") {
						getById("advancedOptionsAudio").style.display = "inline-flex";
					}
					var div = document.createElement("div");
					var label = document.createElement("label");
					var i = "masterGain";
					//label.id = "label_" + i;
					label.htmlFor = "constraints_" + i;
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
					label.style = "display:inline-block;";

					var input = document.createElement("input");
					input.min = 0;
					input.max = 200;

					input.dataset.deviceid = track0.id; // pointless

					input.type = "range";
					input.dataset.keyname = i;
					input.dataset.labelname = label.innerHTML;
					input.id = "constraints_" + i;
					input.style = "display:block; width:100%;";
					input.name = "constraints_" + i;

					input.value = session.webAudios[webAudio].gainNode.gain.value * 100;
					//label.innerHTML += " " + parseInt(session.webAudios[webAudio].gainNode.gain.value * 100);
					input.title = parseInt(input.value);

					var manualInput = document.createElement("input");
					manualInput.type = "number";
					manualInput.dataset.keyname = i;
					manualInput.dataset.deviceid = track0.id;
					manualInput.dataset.labelname = label.innerHTML;
					manualInput.value = session.webAudios[webAudio].gainNode.gain.value * 100;
					manualInput.className = "manualInput";
					manualInput.id = "label_" + i;

					manualInput.onchange = function (e) {
						getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
						changeMainGain(e.target.value);
						e.target.title = e.target.value;
						pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
					};

					input.oninput = function (e) {
						getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
						changeMainGain(e.target.value);
						e.target.title = e.target.value;
					};

					input.onchange = function (e) {
						getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
						changeMainGain(e.target.value);
						e.target.title = e.target.value;
						pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
					};

					getById("popupSelector_constraints_audio").appendChild(div);
					div.appendChild(label);
					div.appendChild(manualInput);
					div.appendChild(input);
					break;
				}
			}
		}

		if (session.micDelay !== false && ii == 0) {
			// ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node)
			if (getById("popupSelector_constraints_audio").style.display == "none") {
				getById("advancedOptionsAudio").style.display = "inline-flex";
			}

			var label = document.createElement("label");
			var i = "micDelay";
			label.htmlFor = "constraints_" + i;
			label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + " (ms):";

			var input = document.createElement("input");
			input.min = 0;
			input.max = 500;

			input.dataset.deviceid = track0.id; // pointless, for now

			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.labelname = label.innerHTML;
			input.id = "constraints_" + i;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;

			for (var webAudio in session.webAudios) {
				if (session.webAudios[webAudio].micDelay) {
					// session.webAudios[waid].micDelay.delayTime.setValueAtTime
					input.value = session.webAudios[webAudio].micDelay.delayTime.value * 1000;
					label.innerHTML += " " + parseInt(session.webAudios[webAudio].micDelay.delayTime.value * 1000);
					input.title = input.value;
					break;
				}
			}

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.dataset.labelname = label.innerHTML;
			manualInput.value = parseFloat(input.value);
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i;

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeMicDelay(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeMicDelay(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeMicDelay(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			getById("popupSelector_constraints_audio").appendChild(label);
			getById("popupSelector_constraints_audio").appendChild(manualInput);
			getById("popupSelector_constraints_audio").appendChild(input);
		}

		if (session.lowcut && ii == 0) {
			// ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node)
			if (getById("popupSelector_constraints_audio").style.display == "none") {
				getById("advancedOptionsAudio").style.display = "inline-flex";
			}

			var label = document.createElement("label");
			var i = "Low_Cut";
			label.htmlFor = "constraints_" + i;
			label.innerText = "Low Cut:";

			var input = document.createElement("input");
			input.min = 50;
			input.max = 400;

			input.dataset.deviceid = track0.id; // pointless

			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.labelname = label.innerHTML;
			input.id = "constraints_" + i;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;

			for (var webAudio in session.webAudios) {
				if (session.webAudios[webAudio].lowcut1) {
					input.value = session.webAudios[webAudio].lowcut1.frequency.value;
					label.innerHTML += " " + session.webAudios[webAudio].lowcut1.frequency.value;
					input.title = input.value;
					break;
				}
			}

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.dataset.labelname = label.innerHTML;
			manualInput.value = parseFloat(input.value);
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i;

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeLowCut(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeLowCut(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeLowCut(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			getById("popupSelector_constraints_audio").appendChild(label);
			getById("popupSelector_constraints_audio").appendChild(manualInput);
			getById("popupSelector_constraints_audio").appendChild(input);
		}

		if (session.equalizer && ii == 0) {
			// ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node)
			if (getById("popupSelector_constraints_audio").style.display == "none") {
				getById("advancedOptionsAudio").style.display = "inline-flex";
			}

			var label = document.createElement("label");
			var i = "Low_EQ";
			//label.id = "label_" + i;
			label.htmlFor = "constraints_" + i;
			label.innerHTML = "Low EQ:";

			var input = document.createElement("input");
			input.min = -50;
			input.max = 50;

			input.dataset.deviceid = track0.id; // pointless

			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.labelname = label.innerHTML;
			input.id = "constraints_" + i;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;

			for (var webAudio in session.webAudios) {
				if (session.webAudios[webAudio].lowEQ) {
					input.value = session.webAudios[webAudio].lowEQ.gain.value;
					label.innerHTML += " " + session.webAudios[webAudio].lowEQ.gain.value;
					input.title = input.value;
				}
			}

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.dataset.labelname = label.innerHTML;
			manualInput.value = parseFloat(input.value);
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i;

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeLowEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeLowEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeLowEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			getById("popupSelector_constraints_audio").appendChild(label);
			getById("popupSelector_constraints_audio").appendChild(manualInput);
			getById("popupSelector_constraints_audio").appendChild(input);
			//
			if (getById("popupSelector_constraints_audio").style.display == "none") {
				getById("advancedOptionsAudio").style.display = "inline-flex";
			}

			var label = document.createElement("label");
			var i = "Mid_EQ";
			//label.id = "label_" + i;
			label.htmlFor = "constraints_" + i;
			label.innerHTML = "Mid EQ:";

			var input = document.createElement("input");
			input.min = -50;
			input.max = 50;

			input.dataset.deviceid = track0.id; // pointless

			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.labelname = label.innerHTML;
			input.id = "constraints_" + i;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;

			for (var webAudio in session.webAudios) {
				if (session.webAudios[webAudio].midEQ) {
					input.value = session.webAudios[webAudio].midEQ.gain.value;
					label.innerHTML += " " + session.webAudios[webAudio].midEQ.gain.value;
					input.title = input.value;
				}
			}

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.dataset.labelname = label.innerHTML;
			manualInput.value = parseFloat(input.value);
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i;

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeMidEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeMidEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeMidEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			getById("popupSelector_constraints_audio").appendChild(label);
			getById("popupSelector_constraints_audio").appendChild(manualInput);
			getById("popupSelector_constraints_audio").appendChild(input);
			//
			if (getById("popupSelector_constraints_audio").style.display == "none") {
				getById("advancedOptionsAudio").style.display = "inline-flex";
			}

			var label = document.createElement("label");
			var i = "High_EQ";
			//label.id = "label_" + i;
			label.htmlFor = "constraints_" + i;
			label.innerHTML = "High EQ:";

			var input = document.createElement("input");
			input.min = -50;
			input.max = 50;

			input.dataset.deviceid = track0.id; // pointless

			input.type = "range";
			input.dataset.keyname = i;
			input.dataset.labelname = label.innerHTML;
			input.id = "constraints_" + i;
			input.style = "display:block; width:100%;";
			input.name = "constraints_" + i;

			for (var webAudio in session.webAudios) {
				if (session.webAudios[webAudio].highEQ) {
					input.value = session.webAudios[webAudio].highEQ.gain.value;
					label.innerHTML += " " + session.webAudios[webAudio].highEQ.gain.value;
					input.title = input.value;
				}
			}

			var manualInput = document.createElement("input");
			manualInput.type = "number";
			manualInput.dataset.keyname = i;
			manualInput.dataset.labelname = label.innerHTML;
			manualInput.value = parseFloat(input.value);
			manualInput.className = "manualInput";
			manualInput.id = "label_" + i;

			manualInput.onchange = function (e) {
				getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeHighEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			input.oninput = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeHighEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
			};

			input.onchange = function (e) {
				getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
				changeHighEQ(e.target.value, e.target.dataset.deviceid);
				e.target.title = e.target.value;
				pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
			};

			getById("popupSelector_constraints_audio").appendChild(label);
			getById("popupSelector_constraints_audio").appendChild(manualInput);
			getById("popupSelector_constraints_audio").appendChild(input);
		}

		if (session.noisegate !== false && ii == 0) {
			for (var webAudio in session.webAudios) {
				if (session.webAudios[webAudio].gatingNode) {
					var div = document.createElement("div");
					var label = document.createElement("label");

					var i = "noiseGating";

					label.id = "label_" + i + "_" + ii;
					label.htmlFor = "constraints_" + i + "_" + ii;
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
					label.style = "display:inline-block;";
					label.dataset.keyname = i;
					label.title = "This will reduce the gain ~80% when there is no one talking loudly";
					var input = document.createElement("select");
					var c = document.createElement("option");

					input.dataset.deviceid = track0.id;

					var opt = new Option("Off", false);
					input.options.add(opt);
					opt = new Option("On", true);
					if (session.noisegate) {
						opt.selected = "true";
					}
					input.options.add(opt);

					if (getById("popupSelector_constraints_audio").style.display == "none") {
						getById("advancedOptionsAudio").style.display = "inline-flex";
					}

					input.id = "constraints_" + i + "_" + ii;
					input.className = "constraintCameraInput";
					input.name = "constraints_" + i + "_" + ii;
					input.style = "display:inline; padding:2px;";
					input.dataset.keyname = i;
					input.dataset.chosen = input.value;
					input.onchange = function (e) {
						this.dataset.chosen = this.value;
						if (e.target.value == "false") {
							session.noisegate = null;
						} else if (e.target.value == "true") {
							session.noisegate = true;
						} else {
							session.noisegate = e.target.value;
						}
						if (!session.noisegate) {
							changeGatingGain(100);
							changeGatingGain(100, 3100);
						}
						pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
					};

					getById("popupSelector_constraints_audio").appendChild(div);
					div.appendChild(label);
					div.appendChild(input);
					break;
				}
			}
		}

		////////
		if (tracks.length > 1) {
			var label = document.createElement("h4");
			label.innerHTML = track0.label;
			label.style = "text-shadow: 0 0 10px #fff3;margin:0px 0 10px 0";
			if (ii > 0) {
				label.style = "text-shadow: 0 0 10px #fff3;margin:40px 0 10px 0";
			}
			getById("popupSelector_constraints_audio").appendChild(label);
		}

		for (var i in session.audioConstraints) {
			try {
				log(i);
				log(session.audioConstraints[i]);

				if (typeof session.audioConstraints[i] === "object" && session.audioConstraints[i] !== null && "max" in session.audioConstraints[i] && "min" in session.audioConstraints[i]) {
					if (i === "aspectRatio") {
						continue;
					} else if (i === "width") {
						continue;
					} else if (i === "height") {
						continue;
					} else if (i === "frameRate") {
						continue;
					} else if (i === "latency") {
						//	continue;
					} else if (i === "sampleRate") {
						//continue;
					} else if (i === "sampleSize") {
						//continue;
					} else if (i === "channelCount") {
						if (!(session.stereo && session.stereo != 3)) {
							// not stereo
							continue;
						}
					} else if (!session.disableWebAudio && i === "volume") {
						continue;
					}

					var label = document.createElement("label");
					//label.id = "label_" + i + "_"+ii;
					label.htmlFor = "constraints_" + i + "_" + ii;
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";

					var input = document.createElement("input");
					input.min = session.audioConstraints[i].min;
					input.max = session.audioConstraints[i].max;

					input.dataset.deviceid = track0.id;

					if (getById("popupSelector_constraints_audio").style.display == "none") {
						getById("advancedOptionsAudio").style.display = "inline-flex";
					}

					var manualInput = document.createElement("input");
					manualInput.type = "number";

					if ("step" in session.audioConstraints[i]) {
						input.step = session.audioConstraints[i].step;
						manualInput.step = session.audioConstraints[i].step;
					} else if ("volume" == i) {
						input.step = 0.01;
						manualInput.step = 0.01;
					}

					if (i in session.currentAudioConstraints) {
						input.value = parseFloat(session.currentAudioConstraints[i]);
						label.title = "Previously was:  " + session.currentAudioConstraints[i];
						input.title = "Previously was:  " + session.currentAudioConstraints[i];
					}

					if (i === "height" || i === "width") {
						input.title = "Hold CTRL (or cmd) to lock width and height together when changing them";
						input.min = 16;
					} else if (i == "sampleRate") {
						label.title = "Audio typically gets resampled to 48-kHz";
					}

					input.type = "range";
					input.dataset.keyname = i;
					input.dataset.track = ii;

					input.id = "constraints_" + i + "_" + ii;
					input.style = "display:block; width:100%;";
					input.name = "constraints_" + i + "_" + ii;

					manualInput.dataset.keyname = i;
					manualInput.dataset.track = ii;
					manualInput.dataset.deviceid = track0.id;

					manualInput.className = "manualInput";
					manualInput.id = "label_" + i + "_" + ii;
					manualInput.max = session.audioConstraints[i].max;
					manualInput.min = session.audioConstraints[i].min;

					manualInput.value = parseFloat(session.currentAudioConstraints[i]);

					manualInput.onchange = function (e) {
						try {
							getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track).value = parseFloat(e.target.value);
							applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid);
							e.target.title = e.target.value;
							pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
						} catch (e) {
							errorlog(e);
						}
					};

					input.onchange = function (e) {
						try {
							getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track).value = parseFloat(e.target.value);
							applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid);
							e.target.title = e.target.value;
							pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
						} catch (e) {
							errorlog(e);
						}
					};

					// not sure if I should include "oninput" as well?  Probably not needed.

					var div = document.createElement("div");
					if (parseFloat(input.min) == parseFloat(input.max)) {
						manualInput.disabled = true;
						manualInput.title = "Only one option available, so can't be changed";
						label.title = "Only one option available, so can't be changed";
						div.appendChild(label);
						div.appendChild(manualInput);
						getById("popupSelector_constraints_audio").appendChild(div);
					} else {
						div.appendChild(label);
						div.appendChild(manualInput);
						div.appendChild(input);
						getById("popupSelector_constraints_audio").appendChild(div);
					}
				} else if (typeof session.audioConstraints[i] === "object" && session.audioConstraints[i] !== null) {
					if (i == "resizeMode") {
						continue;
					}

					var div = document.createElement("div");
					var label = document.createElement("label");
					label.id = "label_" + i + "_" + ii;
					label.htmlFor = "constraints_" + i + "_" + ii;
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
					label.style = "display:inline-block;";
					label.dataset.keyname = i;

					var input = document.createElement("select");
					var c = document.createElement("option");

					if (session.audioConstraints[i].length == 2) {
						for (var opts in session.audioConstraints[i]) {
							log(opts);
							if (session.audioConstraints[i][opts] === true) {
								var opt = new Option("On", session.audioConstraints[i][opts]);
							} else if (session.audioConstraints[i][opts] === false) {
								var opt = new Option("Off", session.audioConstraints[i][opts]);
							} else {
								var opt = new Option(session.audioConstraints[i][opts], session.audioConstraints[i][opts]);
							}
							input.options.add(opt);

							if (i in session.currentAudioConstraints) {
								if (session.audioConstraints[i][opts] == session.currentAudioConstraints[i]) {
									opt.selected = "true";
								}
							}
						}
					} else if (session.audioConstraints[i].length > 1) {
						for (var opts in session.audioConstraints[i]) {
							log(opts);
							var opt = new Option(session.audioConstraints[i][opts], session.audioConstraints[i][opts]);
							input.options.add(opt);

							if (i in session.currentAudioConstraints) {
								if (session.audioConstraints[i][opts] == session.currentAudioConstraints[i]) {
									opt.selected = "true";
								}
							}
						}
					} else if (i.toLowerCase() == "torch") {
						var opt = new Option("Off", false);
						input.options.add(opt);
						opt = new Option("On", true);
						input.options.add(opt);
					} else {
						continue;
					}

					if (getById("popupSelector_constraints_audio").style.display == "none") {
						getById("advancedOptionsAudio").style.display = "inline-flex";
					}

					input.id = "constraints_" + i + "_" + ii;
					input.className = "constraintCameraInput";
					input.name = "constraints_" + i + "_" + ii;
					input.dataset.deviceid = track0.id;
					input.style = "display:inline; padding:2px;";
					input.dataset.keyname = i;
					input.dataset.chosen = input.value;
					input.onchange = function (e) {
						this.dataset.chosen = this.value;
						applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid);
						pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
					};
					getById("popupSelector_constraints_audio").appendChild(div);
					div.appendChild(label);
					div.appendChild(input);
				} else if (typeof session.audioConstraints[i] === "boolean") {
					var div = document.createElement("div");
					var label = document.createElement("label");
					label.id = "label_" + i + "_" + ii;
					label.htmlFor = "constraints_" + i + "_" + ii;
					label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
					label.style = "display:inline-block;";
					label.dataset.keyname = i;
					var input = document.createElement("select");
					var c = document.createElement("option");

					input.dataset.deviceid = track0.id;

					var opt = new Option("Off", false);
					input.options.add(opt);
					opt = new Option("On", true);
					input.options.add(opt);

					if (getById("popupSelector_constraints_audio").style.display == "none") {
						getById("advancedOptionsAudio").style.display = "inline-flex";
					}

					input.id = "constraints_" + i + "_" + ii;
					input.className = "constraintCameraInput";
					input.name = "constraints_" + i + "_" + ii;
					input.style = "display:inline; padding:2px;";
					input.dataset.keyname = i;
					input.dataset.chosen = input.value;
					input.onchange = function (e) {
						this.dataset.chosen = this.value;
						//getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value;
						applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid);
						pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
					};
					getById("popupSelector_constraints_audio").appendChild(div);
					div.appendChild(label);
					div.appendChild(input);
				}
			} catch (e) {
				errorlog(e);
			}
		}

		if (tracks.length > 1) {
			for (var webAudio in session.webAudios) {
				if (session.webAudios[webAudio].subGainNodes && track0.id in session.webAudios[webAudio].subGainNodes) {
					if (getById("popupSelector_constraints_audio").style.display == "none") {
						getById("advancedOptionsAudio").style.display = "inline-flex";
					}
					var div = document.createElement("div");
					var label = document.createElement("label");
					var i = "Gain";
					label.id = "label_" + i + "_" + track0.id;
					label.htmlFor = "constraints_" + i + "_" + track0.id;
					label.innerText = "Gain:";
					label.style = "display:inline-block; padding:0;margin-top: 15px";

					var input = document.createElement("input");
					input.min = 0;
					input.max = 200;

					input.dataset.deviceid = track0.id; // pointless

					input.type = "range";
					input.dataset.keyname = i;
					input.dataset.labelname = label.innerHTML;
					input.id = "constraints_" + i + "_" + track0.id;
					input.style = "display:block; width:100%;";
					input.name = "constraints_" + i + "_" + track0.id;

					input.value = session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100;
					//label.innerText += " " + parseInt(session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100);
					input.title = parseInt(input.value);

					var manualInput = document.createElement("input");
					manualInput.type = "number";
					manualInput.dataset.keyname = i;
					manualInput.dataset.deviceid = track0.id;
					manualInput.dataset.labelname = label.innerHTML;
					manualInput.value = session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100;
					manualInput.className = "manualInput";
					manualInput.id = "manualInput_" + i + "_" + track0.id;

					manualInput.onchange = function (e) {
						getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value);
						//getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value);
						getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value);
						changeSubGain(e.target.value, e.target.dataset.deviceid);
						e.target.title = e.target.value;
						pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
					};

					input.oninput = function (e) {
						//getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value);
						getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value);
						changeSubGain(e.target.value, e.target.dataset.deviceid);
						e.target.title = e.target.value;
					};

					input.onchange = function (e) {
						//getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value);
						getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value);
						changeSubGain(e.target.value, e.target.dataset.deviceid);
						e.target.title = e.target.value;
						pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
					};

					getById("popupSelector_constraints_audio").appendChild(div);
					div.appendChild(label);
					div.appendChild(manualInput);
					div.appendChild(input);
					break;
				}
			}
		}
	}
}

function applyAudioHack(constraint, value = null, deviceid = "default") {
	if (value == parseFloat(value)) {
		value = parseFloat(value);
		if (constraint == "channelCount") {
			session.audioInputChannels = value;
		}
		value = {
			exact: value
		};
	} else if (value == "true") {
		value = true;
	} else if (value == "false") {
		value = false;
	}
	
	try {
		var tracks = session.streamSrc.getAudioTracks();
		if (!tracks.length) {
			warnlog("session.streamSrc contains no audio tracks");
			return;
		}
		
		var track0 = tracks[0];
		for (var ii = 0; ii < tracks.length; ii++) {
			if (tracks[ii].id == deviceid) {
				track0 = tracks[ii];
				break;
			}
		}
		
		if (track0.getCapabilities) {
			session.audioConstraints = track0.getCapabilities();
		} else if (Firefox) {
			// Firefox fallback
			session.audioConstraints = {
				autoGainControl: [true, false],
				deviceId: deviceid,
				echoCancellation: [true, false],
				noiseSuppression: [true, false]
			};
		}
		log(session.audioConstraints);
		
		if (track0.getSettings) {
			session.currentAudioConstraints = track0.getSettings();
		}
	} catch (e) {
		warnlog("Error getting audio track info");
		errorlog(e);
		return;
	}
	
	var new_constraints = Object.assign({}, session.currentAudioConstraints, {
		[constraint]: value
	});
	new_constraints = {
		audio: new_constraints,
		video: false
	};
	log("new constraints");
	log(new_constraints);
	activatedPreview = false;
	
	enumerateDevices()
		.then(gotDevices2)
		.then(function () {
			grabAudio("#audioSource3", null, new_constraints, false, saveAudioResult);
		});
}

// saveAudioResult is disabled but keeping structure for potential future use
function saveAudioResult() {
	return false; // DISABLED: we can't load audio settings, so no point in saving them
	
	/* Future implementation when audio settings can be loaded:
	if (!session.streamSrc) {
		return;
	}
	var tracks = session.streamSrc.getAudioTracks();
	if (!tracks.length) {
		return;
	}
	var track0 = tracks[0];
	session.currentAudioConstraints = track0.getSettings();
	if (session.currentAudioConstraints.deviceId) {
		setStorage("audio_" + session.currentAudioConstraints.deviceId, session.currentAudioConstraints);
	}
	*/
}

function listCameraSettings() {
	getById("popupSelector_constraints_video").innerHTML = "";

	if (session.controlRoomBitrate === true) {
		session.controlRoomBitrate = session.totalRoomBitrate;
	}

	if (session.roomid && session.view !== "" && session.controlRoomBitrate !== false) {
		log("LISTING OPTION FOR BITRATE CONTROL");
		var i = "Room Video Bitrate (kbps)";
		var label = document.createElement("label");

		label.htmlFor = "constraints_" + i;
		label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
		label.title = "If you're on a slow network, you can improve frame rate and audio quality by reducing the amount of video data that others send you";

		var input = document.createElement("input");
		input.min = 0;
		input.max = parseInt(session.totalRoomBitrate*1.5);

		if (getById("popupSelector_constraints_video").style.display == "none") {
			getById("advancedOptionsCamera").style.display = "inline-flex";
		}

		input.value = parseInt(session.controlRoomBitrate);
		label.innerHTML = i + ": ";

		var manualInput = document.createElement("input");
		manualInput.type = "number";
		manualInput.value = session.controlRoomBitrate;
		manualInput.className = "manualInput";
		manualInput.id = "label_" + i;

		input.type = "range";
		input.dataset.keyname = i;
		input.id = "constraints_" + i;
		input.style = "display:block; width:100%;";
		input.name = "constraints_" + i;
		input.title = "If you're on a slow network, you can improve frame rate and audio quality by reducing the amount of video data that others send you";

		manualInput.onchange = function (e) {
			getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
			if (e.target.value > session.totalRoomBitrate) {
				return;
			} else {
				session.controlRoomBitrate = parseInt(e.target.value);
			}
			updateMixer();
			pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
		};

		input.onchange = function (e) {
			getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
			if (e.target.value > session.totalRoomBitrate) {
				return;
			} else {
				session.controlRoomBitrate = parseInt(e.target.value);
			}
			updateMixer();
			pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
		};
		getById("popupSelector_constraints_video").appendChild(label);
		getById("popupSelector_constraints_video").appendChild(manualInput);
		getById("popupSelector_constraints_video").appendChild(input);
	}
	try {
		var track0 = session.streamSrc.getVideoTracks();
		if (track0.length) {
			track0 = track0[0];
			if (track0.getCapabilities) {
				session.cameraConstraints = track0.getCapabilities();
			} else {
				session.cameraConstraints = {}; // probably firefox...
			}
			log(session.cameraConstraints);
		}
	} catch (e) {
		errorlog(e);
		return;
	}

	try {
		if (track0.getSettings) {
			session.currentCameraConstraints = track0.getSettings();
			if (session.mobile) {
				if (screen && screen.orientation && screen.orientation.type) {
					if (!screen.orientation.type.includes("portrait")) {
						if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
							session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
						}
					}
				} else if (!window.matchMedia("(orientation: portrait)").matches) {
					if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
						session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
					}
				}
			}
		} else {
			session.currentCameraConstraints = {};
		}
	} catch (e) {
		errorlog(e);
	}

	var orderedConstraints = {};
	if (session.cameraConstraints.torch) {
		orderedConstraints.torch = session.cameraConstraints.torch;
	}
	if (session.cameraConstraints.aspectRatio) {
		orderedConstraints.aspectRatio = session.cameraConstraints.aspectRatio;
	}
	if (session.cameraConstraints.width) {
		orderedConstraints.width = session.cameraConstraints.width;
	}
	if (session.cameraConstraints.height) {
		orderedConstraints.height = session.cameraConstraints.height;
	}
	if (session.cameraConstraints.zoom) {
		orderedConstraints.zoom = session.cameraConstraints.zoom;
	}
	for (var key in session.cameraConstraints) {
		if (session.cameraConstraints.hasOwnProperty(key) && key !== "width" && key !== "height") {
			orderedConstraints[key] = session.cameraConstraints[key];
		}
	}
	session.cameraConstraints = orderedConstraints;

	for (var i in session.cameraConstraints) {
		try {
			log(i);
			log(session.cameraConstraints[i]);

			if (i === "focusMode") {
				continue; // I'll handle this with FocusDistance instead
			} else if (i === "whiteBalanceMode") {
				continue; // I'll handle this elsewhere
			} else if (i === "exposureMode") {
				continue; // I'll handle this elsewhere
			}

			if (typeof session.cameraConstraints[i] === "object" && session.cameraConstraints[i] !== null && "max" in session.cameraConstraints[i] && "min" in session.cameraConstraints[i]) {
				var manualMode = false;
				var manualLabel = false;
				if (i === "exposureTime") {
					if (session.currentCameraConstraints["exposureMode"]) {
						manualMode = document.createElement("input");
						manualMode.type = "checkbox";
						manualMode.id = "manual_" + i;
						manualMode.dataset.keyname = "exposureMode";
						manualMode.onchange = function (e) {
							var value = "manual";
							if (e.target.checked) {
								value = "continuous";
							}
							if (CtrlPressed) {
								updateCameraConstraints(e.target.dataset.keyname, value, true, false);
							} else {
								updateCameraConstraints(e.target.dataset.keyname, value, false, false);
							}
							pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value });
						};

						manualLabel = document.createElement("label");
						manualLabel.htmlFor = manualMode.id;
						manualLabel.innerHTML = "Auto: ";
						manualLabel.style.marginLeft = "20px";
						if (session.currentCameraConstraints["exposureMode"] == "continuous") {
							manualMode.checked = true;
						}
					}
				} else if (i === "focusDistance") {
					if (session.currentCameraConstraints["focusMode"]) {
						manualMode = document.createElement("input");
						manualMode.type = "checkbox";
						manualMode.id = "manual_" + i;
						manualMode.dataset.keyname = "focusMode";
						manualMode.onchange = function (e) {
							var value = "manual";
							if (e.target.checked) {
								value = "continuous";
							}
							if (CtrlPressed) {
								updateCameraConstraints(e.target.dataset.keyname, value, true, false);
							} else {
								updateCameraConstraints(e.target.dataset.keyname, value, false, false);
							}
							pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value });
						};
						manualLabel = document.createElement("label");
						manualLabel.htmlFor = manualMode.id;
						manualLabel.innerHTML = "Auto: ";
						manualLabel.style.marginLeft = "20px";
						if (session.currentCameraConstraints["focusMode"] == "continuous") {
							manualMode.checked = true;
						}
					}
				} else if (i === "colorTemperature") {
					if (session.currentCameraConstraints["whiteBalanceMode"]) {
						manualMode = document.createElement("input");
						manualMode.type = "checkbox";
						manualMode.id = "manual_" + i;
						manualMode.dataset.keyname = "whiteBalanceMode";
						manualMode.onchange = function (e) {
							var value = "manual";
							if (e.target.checked) {
								value = "continuous";
							}
							if (CtrlPressed) {
								updateCameraConstraints(e.target.dataset.keyname, value, true, false);
							} else {
								updateCameraConstraints(e.target.dataset.keyname, value, false, false);
							}
							pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value });
						};
						manualLabel = document.createElement("label");
						manualLabel.htmlFor = manualMode.id;
						manualLabel.innerHTML = "Auto: ";
						manualLabel.style.marginLeft = "20px";
						if (session.currentCameraConstraints["whiteBalanceMode"] == "continuous") {
							manualMode.checked = true;
						}
					}
				}

				var label = document.createElement("label");
				label.htmlFor = "constraints_" + i;
				label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";

				var input = document.createElement("input");
				input.min = parseFloat(session.cameraConstraints[i].min);

				if (i === "aspectRatio") {
					input.max = 5;
					input.min = 0.2;
				} else {
					input.min = parseFloat(session.cameraConstraints[i].min);
					input.max = parseFloat(session.cameraConstraints[i].max);
				}

				if (parseFloat(input.min) == parseFloat(input.max)) {
					continue;
				}

				if (getById("popupSelector_constraints_video").style.display == "none") {
					getById("advancedOptionsCamera").style.display = "inline-flex";
				}

				var manualInput = document.createElement("input");
				manualInput.type = "number";
				manualInput.dataset.keyname = i;

				manualInput.className = "manualInput";
				manualInput.id = "label_" + i;

				if ("step" in session.cameraConstraints[i]) {
					input.step = session.cameraConstraints[i].step;
					manualInput.step = session.cameraConstraints[i].step;
				} else if (i === "aspectRatio") {
					input.step = 0.000001;
					manualInput.step = 0.005;
				}

				if (i in session.currentCameraConstraints) {
					input.value = parseFloat(session.currentCameraConstraints[i]);
					//label.innerHTML = i + ": " + session.currentCameraConstraints[i];
					manualInput.value = parseFloat(session.currentCameraConstraints[i]);
					label.title = "Previously was:  " + session.currentCameraConstraints[i];
					input.title = "Previously was:  " + session.currentCameraConstraints[i];
				} else {
					label.innerHTML = i;
				}
				if (i === "height" || i === "width") {
					input.title = "Hold CTRL (or cmd) to lock width and height together when changing them";
					input.min = 16;
				}

				input.type = "range";
				input.dataset.keyname = i;
				input.id = "constraints_" + i;
				input.style = "display:block; width:100%;";
				input.name = "constraints_" + i;

				// on manualInput.change = .. update the input field! gotta riprocate

				manualInput.onchange = function (e) {
					getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
					updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false);
					pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
				};

				input.oninput = function (e) {
					getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
					if (CtrlPressed) {
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false);
					} else {
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false);
					}
				};

				input.onchange = function (e) {
					getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
					if (CtrlPressed) {
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false);
					} else {
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false);
					}
					pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
				};

				getById("popupSelector_constraints_video").appendChild(label);
				getById("popupSelector_constraints_video").appendChild(manualInput);
				if (manualMode && manualLabel) {
					getById("popupSelector_constraints_video").appendChild(manualLabel);
					getById("popupSelector_constraints_video").appendChild(manualMode);
				}

				if (i === "aspectRatio") {
					var preSelectButton = document.createElement("button");
					preSelectButton.value = 16 / 9.0;
					preSelectButton.innerText = "16:9";
					preSelectButton.dataset.keyname = i;
					preSelectButton.className = "preSelectButton";
					preSelectButton.onclick = function (e) {
						getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
						getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false);
					};
					getById("popupSelector_constraints_video").appendChild(preSelectButton);
					var preSelectButton = document.createElement("button");
					preSelectButton.value = 9 / 16.0;
					preSelectButton.innerText = "9:16";
					preSelectButton.className = "preSelectButton";
					preSelectButton.dataset.keyname = i;
					preSelectButton.onclick = function (e) {
						getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
						getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false);
					};
					getById("popupSelector_constraints_video").appendChild(preSelectButton);
				}

				getById("popupSelector_constraints_video").appendChild(input);
			} else if (typeof session.cameraConstraints[i] === "object" && session.cameraConstraints[i] !== null) {
				if (i == "resizeMode") {
					continue;
				}

				var div = document.createElement("div");
				var label = document.createElement("label");
				label.id = "label_" + i;
				label.htmlFor = "constraints_" + i;
				label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
				label.style = "display:inline-block;";
				label.dataset.keyname = i;
				var input = document.createElement("select");

				if (session.cameraConstraints[i].length > 1) {
					var included = false;
					for (var opts in session.cameraConstraints[i]) {
						log(opts);
						var opt = new Option(session.cameraConstraints[i][opts], session.cameraConstraints[i][opts]);
						input.options.add(opt);
						if (i in session.currentCameraConstraints) {
							if (session.cameraConstraints[i][opts] == session.currentCameraConstraints[i]) {
								opt.selected = "true";
								included = true;
							}
						}
					}
					if (!included) {
						if (i in session.currentCameraConstraints) {
							var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]);
							input.options.add(opt);
							opt.selected = "true";
						}
					}
				} else if (i.toLowerCase() == "torch") {
					warnlog("TORCH");
					var opt = new Option("Off", false);
					input.options.add(opt);
					opt = new Option("On", true);
					input.options.add(opt);
					try {
						if (session.currentCameraConstraints[i]) {
							opt.selected = "selected";
						}
					} catch (e) {}
				} else if (session.cameraConstraints[i].length && "continuous" == session.cameraConstraints[i][0]) {
					var opt = new Option("continuous", "continuous");
					input.options.add(opt);
					if (i in session.currentCameraConstraints) {
						if ("continuous" == session.currentCameraConstraints[i]) {
							opt.selected = "true";
							var opt = new Option("manual", "manual");
							input.options.add(opt);
							var opt = new Option("none", "none");
							input.options.add(opt);
						} else {
							var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]);
							input.options.add(opt);
							opt.selected = "true";
							if (session.currentCameraConstraints[i] == "none") {
								var opt = new Option("manual", "manual");
								input.options.add(opt);
							} else {
								var opt = new Option("none", "none");
								input.options.add(opt);
							}
						}
					} else {
						opt.selected = "true";
						var opt = new Option("manual", "manual");
						input.options.add(opt);
						var opt = new Option("none", "none");
						input.options.add(opt);
					}
				} else if (session.cameraConstraints[i].length && "manual" == session.cameraConstraints[i][0]) {
					var opt = new Option("manual", "manual");
					input.options.add(opt);
					if (i in session.currentCameraConstraints) {
						if ("manual" == session.currentCameraConstraints[i]) {
							opt.selected = "true";
							var opt = new Option("continuous", "continuous");
							input.options.add(opt);
							var opt = new Option("none", "none");
							input.options.add(opt);
						} else {
							var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]);
							input.options.add(opt);
							opt.selected = "true";
							if (session.currentCameraConstraints[i] == "none") {
								var opt = new Option("continuous", "continuous");
								input.options.add(opt);
							} else {
								var opt = new Option("none", "none");
								input.options.add(opt);
							}
						}
					} else {
						opt.selected = "true";
						var opt = new Option("continuous", "continuous");
						input.options.add(opt);
						var opt = new Option("none", "none");
						input.options.add(opt);
					}
				} else {
					continue;
				}

				if (getById("popupSelector_constraints_video").style.display == "none") {
					getById("advancedOptionsCamera").style.display = "inline-flex";
				}

				input.id = "constraints_" + i;
				input.className = "constraintCameraInput";
				input.name = "constraints_" + i;
				input.style = "display:inline; padding:2px;";
				input.dataset.keyname = i;
				input.dataset.chosen = input.value;
				input.onchange = function (e) {
					this.dataset.chosen = this.value;
					//getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value;
					if (CtrlPressed) {
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false);
					} else {
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false);
					}
					pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
				};
				getById("popupSelector_constraints_video").appendChild(div);
				div.appendChild(label);
				div.appendChild(input);
			} else if (typeof session.cameraConstraints[i] === "boolean") {
				var div = document.createElement("div");
				var label = document.createElement("label");
				label.id = "label_" + i;
				label.htmlFor = "constraints_" + i;
				label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":";
				label.style = "display:inline-block;";
				label.dataset.keyname = i;
				var input = document.createElement("select");

				var opt = new Option("Off", "false");
				input.options.add(opt);

				opt = new Option("On", "true");
				input.options.add(opt);
				if (session.currentCameraConstraints[i]) {
					opt.selected = "true";
				}

				if (getById("popupSelector_constraints_video").style.display == "none") {
					getById("advancedOptionsCamera").style.display = "inline-flex";
				}

				input.id = "constraints_" + i;
				input.className = "constraintCameraInput";
				input.name = "constraints_" + i;
				input.style = "display:inline; padding:2px;";
				input.dataset.keyname = i;
				input.dataset.chosen = input.value;
				input.onchange = function (e) {
					this.dataset.chosen = this.value;
					//getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value;
					if (CtrlPressed) {
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false);
					} else {
						updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false);
					}
					pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value });
				};
				getById("popupSelector_constraints_video").appendChild(div);
				div.appendChild(label);
				div.appendChild(input);
			}
		} catch (e) {
			errorlog(e);
		}
	}

	if (session.currentCameraConstraints.deviceId) {
		if (getStorage("camera_" + session.currentCameraConstraints.deviceId)) {
			var button = document.createElement("button");
			button.innerHTML = "Reset video settings to default";
			button.style.display = "block";
			button.style.padding = "10px 5px";
			button.dataset.deviceId = session.currentCameraConstraints.deviceId;
			button.onclick = function () {
				var deviceId = this.dataset.deviceId;
				var cameraSettings = getStorage("camera_" + deviceId);
				var constraints = {};
				var resetResolution = false;
				var failed = false;
				if (cameraSettings["default"]) {
					if (cameraSettings["current"]) {
						for (var i in cameraSettings["default"]) {
							if (i == "groupId") {
								continue;
							} else if (i === "aspectRatio") {
								// do not load from storage; causes issues
								continue;
							} else if (i === "width") {
								//	continue;
							} else if (i === "height") {
								//	continue;
							} else {
								// if I include any of these, it will complain about mixing types and fail
								if (i in cameraSettings["current"]) {
									if (cameraSettings["current"][i] != cameraSettings["default"][i]) {
										track0
											.applyConstraints({
												advanced: [{ [i]: cameraSettings["default"][i] }]
											})
											.then(() => {})
											.catch(e => {
												errorlog("Failed to reset to defaults");
												failed = true;
											});
									}
								}
								continue;
							}

							if (i in cameraSettings["current"]) {
								if (cameraSettings["current"][i] != cameraSettings["default"][i]) {
									if (i in session.cameraConstraints) {
										if ("min" in session.cameraConstraints[i]) {
											if (session.cameraConstraints[i].min > cameraSettings["default"][i]) {
												continue;
											}
										}
										if ("max" in session.cameraConstraints[i]) {
											if (session.cameraConstraints[i].max < cameraSettings["default"][i]) {
												continue;
											}
										}
									}
									constraints[i] = cameraSettings["default"][i];
									if (i == "width" || i == "height" || i == "aspectRatio") {
										resetResolution = true;
									}
								}
							}
						}
					}
				}
				warnlog(constraints);
				if (Object.keys(constraints).length) {
					track0
						.applyConstraints({
							advanced: [constraints]
						})
						.then(() => {
							if (!failed) {
								removeStorage("camera_" + deviceId);
							}
							listCameraSettings();

							if (resetResolution) {
								session.setResolution(); // this will reset scaling for all viewers of this stream
							}
						})
						.catch(e => {
							errorlog("Failed to reset to defaults");
							errorlog(e);
						});
				} else if (!failed) {
					removeStorage("camera_" + deviceId);
					listCameraSettings();
				}
			};

			getById("popupSelector_constraints_video").appendChild(button);
		}
	}
}

// Audio settings application
function applySavedAudioSettings(track0) {
	if (!track0?.getSettings) return;
	
	log("applySavedAudioSettings");
	session.currentAudioConstraints = track0.getSettings();
	
	const deviceId = session.currentAudioConstraints.deviceId;
	if (!deviceId) return;
	
	const audioSettings = getStorage("audio_" + deviceId);
	if (!audioSettings?.deviceId) return;
	
	const constraints = {};
	const allowedProps = ["autoGainControl", "echoCancellation", "noiseSuppression"];
	
	for (const prop in session.currentAudioConstraints) {
		if (audioSettings[prop] !== undefined && 
			audioSettings[prop] !== session.currentAudioConstraints[prop] &&
			allowedProps.includes(prop)) {
			constraints[prop] = audioSettings[prop];
			warnlog("DIFF: " + prop);
		}
	}
	
	warnlog(constraints);
	if (!Object.keys(constraints).length) return;
	
	track0.applyConstraints({ advanced: [constraints] })
		.then(() => warnlog("audio settings updated for deviceId:" + deviceId))
		.catch(e => errorlog("Failed to reset to audio defaults"));
}

// Video settings application
function applySavedVideoSettings(track0) {
	if (!track0?.getSettings) return;
	
	session.currentCameraConstraints = track0.getSettings();
	
	// Handle mobile orientation
	if (session.mobile) {
		const isPortrait = (screen?.orientation?.type?.includes("portrait")) || 
						   window.matchMedia("(orientation: portrait)").matches;
		if (!isPortrait && session.currentCameraConstraints?.aspectRatio) {
			session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
		}
	}
	
	const deviceId = session.currentCameraConstraints.deviceId;
	if (!deviceId) return;
	
	const cameraSettings = getStorage("camera_" + deviceId);
	if (!cameraSettings?.current) return;
	
	const constraints = {};
	const skipProps = ["groupId"];
	const urlOverrides = {
		aspectRatio: session.forceAspectRatio,
		whiteBalanceMode: session.whiteBalance,
		colorTemperature: session.whiteBalance,
		exposureTime: session.exposure,
		exposureMode: session.exposure,
		zoom: session.zoom,
		saturation: session.saturation,
		sharpness: session.sharpness,
		contrast: session.contrast,
		brightness: session.brightness
	};
	
	for (const prop in session.currentCameraConstraints) {
		if (!cameraSettings.current[prop] || 
			cameraSettings.current[prop] === session.currentCameraConstraints[prop] ||
			skipProps.includes(prop)) continue;
			
		if (urlOverrides[prop]) {
			log(`${prop} is manually set via URL`);
			continue;
		}
		
		constraints[prop] = cameraSettings.current[prop];
		warnlog("DIFF: " + prop);
	}
	
	warnlog(constraints);
	if (!Object.keys(constraints).length) return;
	
	track0.applyConstraints({ advanced: [constraints] })
		.then(() => warnlog("video settings updated for deviceId:" + deviceId))
		.catch(e => errorlog("Failed to reset to defaults"));
}

// Camera constraints update state
var updateCameraConstraintsBusy = false;
var updateCameraConstraintsNext = false;

// Main camera constraints update function
async function updateCameraConstraints(constraint, value = null, ctrl = false, UUID = false, save = true) {
	if (constraint === "zoom" && value === 0) {
		log("can't zoom to zero");
		return;
	}

	log("updateCameraConstraintsBusy?");
	
	if (updateCameraConstraintsBusy) {
		updateCameraConstraintsNext = [constraint, value, ctrl, UUID, save];
		return;
	}
	
	updateCameraConstraintsBusy = true;
	updateCameraConstraintsNext = false;

	try {
		const track0 = session.streamSrc?.getVideoTracks()?.[0];
		
		if (!track0 || track0.readyState !== "live" || !track0.enabled) {
			if (!save) {
				errorlog("TRACK IS NOT ENABLED");
				updateCameraConstraintsBusy = false;
				updateCameraConstraintsNext = false;
			}
			return;
		}

		// Parse value
		if (value == parseFloat(value)) {
			value = parseFloat(value);
		} else if (value === "true") {
			value = true;
		} else if (value === "false") {
			value = false;
		}
		
		log({ advanced: [{ [constraint]: value }] });

		// Get current settings and prepare storage
		let cameraSettings = {};
		if (track0.getSettings) {
			session.currentCameraConstraints = track0.getSettings();
			
			if (session.currentCameraConstraints.deviceId) {
				const storageKey = "camera_" + session.currentCameraConstraints.deviceId;
				const stored = getStorage(storageKey);
				
				if (!stored) {
					cameraSettings.default = JSON.parse(JSON.stringify(session.currentCameraConstraints));
					log(cameraSettings.default);
				} else {
					cameraSettings = stored;
				}
			}
		}

		// Build constraints
		const constraints = await buildConstraints(constraint, value, ctrl, track0);
		
		// Handle mobile orientation for constraints
		if (session.mobile) {
			adjustConstraintsForMobileOrientation(constraints);
		}

		log("20788");
		log(constraints);

		// Apply constraints
		await track0.applyConstraints({ advanced: [constraints] })
			.then(() => {
				log("applied constraint");
				
				if (save) {
					saveConstraintSettings(track0, cameraSettings, constraint, UUID);
				}
				
				if (updateCameraConstraintsNext) {
					setTimeout(() => {
						updateCameraConstraintsBusy = false;
						updateCameraConstraints(...updateCameraConstraintsNext);
					}, 30);
				} else {
					updateCameraConstraintsBusy = false;
				}
			})
			.catch(e => {
				errorlog(e.message);
				errorlog("couldn't save defaults");
				window.focus();
				updateCameraConstraintsBusy = false;
				updateCameraConstraintsNext = false;
			});
			
	} catch (e) {
		errorlog(e);
		updateCameraConstraintsBusy = false;
		updateCameraConstraintsNext = false;
		return e;
	}
}

// Helper to build constraints based on type
async function buildConstraints(constraint, value, ctrl, track0) {
	const current = session.currentCameraConstraints;
	let constraints = {};

	switch (constraint) {
		case "width":
			constraints.width = value;
			if (current?.frameRate) constraints.frameRate = current.frameRate;
			if (!ctrl && current?.height) constraints.height = current.height;
			break;
			
		case "height":
			constraints.height = value;
			if (current?.frameRate) constraints.frameRate = current.frameRate;
			if (!ctrl && current?.width) constraints.width = current.width;
			break;
			
		case "frameRate":
			if (!ctrl) {
				constraints.frameRate = value;
				if (current?.height && current?.width) {
					constraints.height = current.height;
					constraints.width = current.width;
				}
			} else {
				constraints.frameRate = value;
			}
			break;
			
		case "exposureMode":
			if (value === "manual") {
				await applyCurrentSetting(track0, "exposureTime", current);
				constraints = buildManualModeConstraints(constraint, value, "exposureTime", current);
			} else {
				constraints[constraint] = value;
			}
			break;
			
		case "exposureTime":
			constraints[constraint] = value;
			constraints.exposureMode = "manual";
			break;
			
		case "focusMode":
			if (value === "manual") {
				await applyCurrentSetting(track0, "focusDistance", current);
				constraints = buildManualModeConstraints(constraint, value, "focusDistance", current);
			} else {
				constraints[constraint] = value;
			}
			break;
			
		case "focusDistance":
			constraints[constraint] = value;
			constraints.focusMode = "manual";
			break;
			
		case "whiteBalanceMode":
			if (value === "manual") {
				await applyCurrentSetting(track0, "colorTemperature", current);
				constraints = buildWhiteBalanceConstraints(constraint, value, current);
			} else if (value === "continuous") {
				constraints[constraint] = value;
				if (session.mobile && ChromiumVersion) {
					constraints.colorTemperature = 5000;
				}
			} else {
				constraints[constraint] = value;
			}
			break;
			
		case "colorTemperature":
			constraints[constraint] = value;
			constraints.whiteBalanceMode = "manual";
			break;
			
		case "aspectRatio":
			constraints[constraint] = value;
			if (current?.frameRate) constraints.frameRate = current.frameRate;
			if (session.mobile) {
				const isPortrait = (screen?.orientation?.type?.includes("portrait")) || 
								   window.matchMedia("(orientation: portrait)").matches;
				if (isPortrait && constraints.aspectRatio) {
					constraints.aspectRatio = 1 / constraints.aspectRatio;
				}
			}
			break;
			
		default:
			constraints[constraint] = value;
	}
	
	return constraints;
}

// Helper for manual mode constraints
function buildManualModeConstraints(constraint, value, dependentProp, current) {
	const constraints = { [constraint]: value };
	
	if (current?.height && current?.width) {
		constraints.height = current.height;
		constraints.width = current.width;
	}
	
	if (current?.[dependentProp]) {
		constraints[dependentProp] = current[dependentProp];
	}
	
	return constraints;
}

// Helper for white balance constraints
function buildWhiteBalanceConstraints(constraint, value, current) {
	const constraints = { [constraint]: value };
	
	if (current?.height && current?.width) {
		constraints.height = current.height;
		constraints.width = current.width;
	}
	
	const colorTempConstraints = session.cameraConstraints?.colorTemperature;
	if (colorTempConstraints?.max && colorTempConstraints?.min) {
		if (current?.colorTemperature) {
			constraints.colorTemperature = current.colorTemperature;
		} else if (5000 >= colorTempConstraints.min && 5000 <= colorTempConstraints.max) {
			constraints.colorTemperature = 5000;
		} else {
			constraints.colorTemperature = colorTempConstraints.max;
		}
	}
	
	return constraints;
}

// Helper to apply current setting
async function applyCurrentSetting(track0, prop, current) {
	if (!current?.[prop]) return;
	
	const tempConstraints = { [prop]: current[prop] };
	await track0.applyConstraints({ advanced: [tempConstraints] });
	session.currentCameraConstraints = track0.getSettings();
}

// Helper to adjust constraints for mobile orientation
function adjustConstraintsForMobileOrientation(constraints) {
	const isPortrait = (screen?.orientation?.type?.includes("portrait")) || 
					   window.matchMedia("(orientation: portrait)").matches;
					   
	if (!isPortrait) return;
	
	if (constraints.width && constraints.height) {
		[constraints.width, constraints.height] = [constraints.height, constraints.width];
	} else if (constraints.width) {
		constraints.height = constraints.width;
		delete constraints.width;
		if (!constraints.aspectRatio && session.currentCameraConstraints?.height) {
			constraints.width = session.currentCameraConstraints.height;
		}
	} else if (constraints.height) {
		constraints.width = constraints.height;
		delete constraints.height;
		if (!constraints.aspectRatio && session.currentCameraConstraints?.width) {
			constraints.height = session.currentCameraConstraints.width;
		}
	}
}

// Helper to save constraint settings
function saveConstraintSettings(track0, cameraSettings, constraint, UUID) {
	if (!track0.getSettings || !session.currentCameraConstraints.deviceId) return;
	
	session.currentCameraConstraints = track0.getSettings();
	cameraSettings.current = session.currentCameraConstraints;
	setStorage("camera_" + session.currentCameraConstraints.deviceId, cameraSettings);
	
	if (toggleSettingsState === true) {
		listCameraSettings();
	}
	
	if (UUID) {
		const data = {
			UUID: UUID,
			videoOptions: listVideoSettingsPrep()
		};
		sendMediaDevices(data.UUID);
		session.sendMessage(data, data.UUID);
	}
	
	if (["width", "height", "aspectRatio"].includes(constraint)) {
		session.setResolution();
	}
}


function setupSharpnessTool() {
	var promise;
	const worker = new Worker("./thirdparty/focus_worker.js", { type: "module" });
	worker.onerror = event => {
		errorlog(event);
		promise.reject(event);
	};
	worker.onmessage = messageEvent => {
		log("Sharpness score: " + messageEvent.data.score.avg_edge_width_perc);
		promise.resolve(messageEvent.data.score.avg_edge_width_perc);
	};

	measureBlur = imageData => {
		worker.postMessage({ imageData });
	};

	const canvas = document.createElement("canvas");
	// document.getElementById("header").appendChild(canvas);

	async function getSharpness(x = 50, y = 50) {
		if (session.videoElement) {
			log("XY");
			log(x + " : " + y);
			canvas.width = session.videoElement.videoWidth / 5;
			canvas.height = session.videoElement.videoHeight / 5;

			if (x < 10) {
				x = 10;
			}
			if (y < 10) {
				y = 10;
			}
			if (x > 90) {
				x = 90;
			}
			if (y > 90) {
				y = 90;
			}

			var sx = (session.videoElement.videoWidth / 100) * (x - 10);
			var sy = (session.videoElement.videoHeight / 100) * (y - 10);
			var sw = session.videoElement.videoWidth * 0.2;
			var sh = session.videoElement.videoHeight * 0.2;

			canvas.getContext("2d").filter = "blur(3px)"; // denoise
			canvas.getContext("2d").drawImage(session.videoElement, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); // for drawing the video element on the canvas

			const canvasData = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height);

			var res, rej;
			promise = new Promise((resolve, reject) => {
				res = resolve;
				rej = reject;
			});
			promise.resolve = res;
			promise.reject = rej;

			measureBlur(canvasData);

			return promise;
		}
		return null;
	}

	return getSharpness;
}
var sharpnessToolActive = false;
var sharpnessTool = false;
async function tapToFocus(x, y, force = false) {
	if (isNaN(x) || isNaN(y)) {
		return;
	}

	if (sharpnessToolActive) {
		return;
	}

	if (!session.streamSrc) {
		checkBasicStreamsExist();
		return;
	}

	//var bestFocus = -1;
	var track0 = session.streamSrc.getVideoTracks();
	if (!track0.length) {
		log("No video tracks");
		return;
	}
	track0 = track0[0];
	if (!track0.getCapabilities) {
		log("Track lacks advanced features. Firefox?");
		return;
	}

	var capabilities = track0.getCapabilities();
	if (!("focusDistance" in capabilities)) {
		log("Track doesn't support focusing");
		return;
	}

	var settings = track0.getSettings();
	if ("focusMode" in settings) {
		if (!force && settings.focusMode !== "manual") {
			log("Need to be in manual focus mode");
			return;
		}
	}

	if (!sharpnessTool) {
		sharpnessTool = setupSharpnessTool();
	}

	var bestFocus = -1;
	var bestSharpness = 999;
	sharpnessToolActive = true;

	try {
		log("Current focus distance: " + capabilities.focusDistance);
		await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: capabilities.focusDistance.min }] });
		await sleep(250);

		var stepping = capabilities.focusDistance.step || 0.1;

		if ((capabilities.focusDistance.max - capabilities.focusDistance.min) / stepping > 100) {
			stepping = parseInt((capabilities.focusDistance.max - capabilities.focusDistance.min) / 100);
		}
		if (!stepping) {
			stepping = 0.1;
		}
		for (var i = capabilities.focusDistance.min; i <= capabilities.focusDistance.max; i += stepping) {
			await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: i }] });
			await sleep(120); // wait long enough for a new frame and focus to adjust.
			log("focus: " + i + ", " + x + "x" + y);
			var response = await sharpnessTool(x, y);
			if (response && response < bestSharpness) {
				bestSharpness = response;
				bestFocus = i;
			} else if (response === null) {
				return;
			}

			log(response + " " + bestSharpness + " " + bestFocus + " " + i + " " + capabilities.focusDistance.max);
		}
		if (bestFocus !== -1) {
			log("Setting focus now to: " + bestFocus);
			await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: bestFocus }] });
		}
	} catch (e) {
		errorlog(e);
	}
	sharpnessToolActive = false;
}

session.remoteFocus = async function (focusDistance, absolute = false) {
    try {
        var track0 = session.streamSrc.getVideoTracks()[0];
        if (!track0?.getCapabilities) return;
        
        var capabilities = track0.getCapabilities();
        if (!capabilities.focusDistance) {
            warnlog("No Focus supported on this device");
            return;
        }

        const focusRange = capabilities.focusDistance;
        if (!("min" in focusRange)) return;

        if (session.focusDistance === false || session.focusDistance === undefined) {
            const settings = track0.getSettings();
            session.focusDistance = settings.focusDistance || focusRange.min;
        }

        let newFocusDistance;
        if (absolute) {
            newFocusDistance = focusRange.min + focusDistance * (focusRange.max - focusRange.min);
        } else {
            const range = focusRange.max - focusRange.min;
            const step = focusRange.step || 0.01;
            const change = Math.max(Math.abs(range * focusDistance), step);
            newFocusDistance = session.focusDistance + (focusDistance > 0 ? change : -change);
        }

        newFocusDistance = Math.min(Math.max(newFocusDistance, focusRange.min), focusRange.max);
        
        const step = focusRange.step || 0.01;
        const steps = Math.round((newFocusDistance - focusRange.min) / step);
        newFocusDistance = focusRange.min + (steps * step);

        // Use updateCameraConstraints with save=false to avoid debouncing
        await updateCameraConstraints("focusDistance", newFocusDistance, false, false, false);
        session.focusDistance = newFocusDistance;
        
        return session.focusDistance;
    } catch (e) {
        errorlog(e);
        return null;
    }
};

session.remoteZoom = async function(zoom, absolute=false) {
    try {
        var track0 = session.streamSrc.getVideoTracks()[0];
        if (!track0?.getCapabilities) return;
        
        var capabilities = track0.getCapabilities();
        if (!capabilities.zoom) {
            warnlog("No zoom supported on this device");
            return;
        }

        const zoomRange = capabilities.zoom;
        if (!("min" in zoomRange) || !("max" in zoomRange) || zoomRange.max === zoomRange.min) {
            warnlog("Zoom not adjustable on this device");
            return;
        }

        if (session.zoom === false || session.zoom === undefined) {
            const settings = track0.getSettings();
            session.zoom = settings.zoom || zoomRange.min;
        }

        let newZoom;
        if (absolute) {
            newZoom = zoomRange.min + zoom * (zoomRange.max - zoomRange.min);
        } else {
            const range = zoomRange.max - zoomRange.min;
            const step = zoomRange.step || 1;
            const change = Math.max(Math.abs(range * zoom), step);
            newZoom = session.zoom + (zoom > 0 ? change : -change);
        }

        newZoom = Math.min(Math.max(newZoom, zoomRange.min), zoomRange.max);
        
        const step = zoomRange.step || 1;
        const steps = Math.round((newZoom - zoomRange.min) / step);
        newZoom = zoomRange.min + (steps * step);

        // Use updateCameraConstraints with save=false
        await updateCameraConstraints("zoom", newZoom, false, false, false);
        session.zoom = newZoom;
        
        return session.zoom;
    } catch (e) {
        errorlog(e);
        return null;
    }
};

session.remotePan = async function(pan, absolute = false) {
    try {
        var track0 = session.streamSrc.getVideoTracks()[0];
        if (!track0?.getCapabilities) return;
        
        var capabilities = track0.getCapabilities();
        if (!capabilities.pan) {
            warnlog("No pan supported on this device");
            return;
        }

        const panRange = capabilities.pan;
        if (!("min" in panRange) || !("max" in panRange) || panRange.max === panRange.min) {
            warnlog("Pan not adjustable on this device");
            return;
        }

        if (session.pan === false || session.pan === undefined) {
            const settings = track0.getSettings();
            session.pan = settings.pan || (panRange.min + panRange.max) / 2;
        }

        let newPan;
        if (absolute) {
            const range = panRange.max - panRange.min;
            newPan = panRange.min + ((pan + 1) / 2) * range;
        } else {
            const range = panRange.max - panRange.min;
            const step = panRange.step || 1;
            const change = Math.max(Math.abs(range * pan), step);
            newPan = session.pan + (pan > 0 ? change : -change);
        }

        newPan = Math.min(Math.max(newPan, panRange.min), panRange.max);
        
        const step = panRange.step || 1;
        const steps = Math.round((newPan - panRange.min) / step);
        newPan = panRange.min + (steps * step);

        // Use updateCameraConstraints with save=false
        await updateCameraConstraints("pan", newPan, false, false, false);
        session.pan = newPan;
        
        return session.pan;
    } catch (e) {
        errorlog(e);
        return null;
    }
};

session.remoteTilt = async function(tilt, absolute = false) {
    try {
        var track0 = session.streamSrc.getVideoTracks()[0];
        if (!track0?.getCapabilities) return;
        
        var capabilities = track0.getCapabilities();
        if (!capabilities.tilt) {
            warnlog("No tilt supported on this device");
            return;
        }

        const tiltRange = capabilities.tilt;
        if (!("min" in tiltRange) || !("max" in tiltRange) || tiltRange.max === tiltRange.min) {
            warnlog("Tilt not adjustable on this device");
            return;
        }

        if (session.tilt === false || session.tilt === undefined) {
            const settings = track0.getSettings();
            session.tilt = settings.tilt || (tiltRange.min + tiltRange.max) / 2;
        }

        let newTilt;
        if (absolute) {
            const range = tiltRange.max - tiltRange.min;
            newTilt = tiltRange.min + ((tilt + 1) / 2) * range;
        } else {
            const range = tiltRange.max - tiltRange.min;
            const step = tiltRange.step || 1;
            const change = Math.max(Math.abs(range * tilt), step);
            newTilt = session.tilt + (tilt > 0 ? change : -change);
        }

        newTilt = Math.min(Math.max(newTilt, tiltRange.min), tiltRange.max);
        
        const step = tiltRange.step || 1;
        const steps = Math.round((newTilt - tiltRange.min) / step);
        newTilt = tiltRange.min + (steps * step);

        // Use updateCameraConstraints with save=false
        await updateCameraConstraints("tilt", newTilt, false, false, false);
        session.tilt = newTilt;
        
        return session.tilt;
    } catch (e) {
        errorlog(e);
        return null;
    }
};

session.remoteExposure = async function (exposure, absolute = false) {
    try {
        var track0 = session.streamSrc.getVideoTracks()[0];
        if (!track0?.getCapabilities) return;
        
        var capabilities = track0.getCapabilities();
        var settings = track0.getSettings();

        if (!capabilities.exposureMode || !capabilities.exposureTime) {
            warnlog("Exposure control not supported on this device");
            return;
        }

        // Ensure manual mode
        if (settings.exposureMode !== 'manual') {
            await updateCameraConstraints("exposureMode", "manual", false, false, false);
        }

        const exposureRange = capabilities.exposureTime;
        
        if (session.exposure === false || session.exposure === undefined) {
            session.exposure = settings.exposureTime || exposureRange.min;
        }

        let newExposure;
        if (absolute) {
            newExposure = exposureRange.min + exposure * (exposureRange.max - exposureRange.min);
        } else {
            const range = exposureRange.max - exposureRange.min;
            const step = exposureRange.step || 1;
            const change = Math.max(Math.abs(range * exposure), step);
            newExposure = session.exposure + (exposure > 0 ? change : -change);
        }

        newExposure = Math.min(Math.max(newExposure, exposureRange.min), exposureRange.max);

        // Use updateCameraConstraints with save=false
        await updateCameraConstraints("exposureTime", newExposure, false, false, false);
        session.exposure = newExposure;

        log(`Applied new exposure time: ${session.exposure}`);
        
        return session.exposure;
    } catch (e) {
        errorlog(e);
        return null;
    }
};


function toggleAudioUser(ele) {
	if (!ele) {
		ele = ele || getById("advancedOptionsAudio");
		ele.style.display = "inline-flex";
		if (getById("popupSelector_constraints_audio").style.display == "block") {
			toggleSettings();
		} else {
			getById("popupSelector_constraints_audio").style.display = "block";
			ele.classList.add("highlight");
			if (!toggleSettingsState) {
				toggleSettings();
			}
		}
	} else {
		ele = ele || getById("advancedOptionsAudio");
		toggle(getById("popupSelector_constraints_audio"), false, false);
		ele.classList.toggle("highlight");
	}

	getById("popupSelector_constraints_loading").style.visibility = "visible";
	getById("popupSelector_constraints_video").style.display = "none";
	getById("popupSelector_user_settings").style.display = "none";
}
function toggleVideoUser(ele) {
	if (!ele) {
		ele = ele || getById("advancedOptionsCamera");
		ele.style.display = "inline-flex";
		if (getById("popupSelector_constraints_video").style.display == "block") {
			toggleSettings();
		} else {
			getById("popupSelector_constraints_video").style.display = "block";
			ele.classList.add("highlight");
			if (!toggleSettingsState) {
				toggleSettings();
			}
		}
	} else {
		ele = ele || getById("advancedOptionsCamera");
		toggle(getById("popupSelector_constraints_video"), false, false);
		ele.classList.toggle("highlight");
	}

	getById("popupSelector_constraints_loading").style.visibility = "visible";
	getById("popupSelector_constraints_audio").style.display = "none";
	getById("popupSelector_user_settings").style.display = "none";
}
function toggleUserUser(ele) {
	ele = ele || getById("advancedOptionsGeneral");
	if (!toggleSettingsState) {
		toggleSettings();
	}
	ele.classList.toggle("highlight");
	toggle(getById("popupSelector_user_settings"), false, false);
	getById("popupSelector_user_settings").style.visibility = "visible";
	getById("popupSelector_constraints_video").style.display = "none";
	getById("popupSelector_constraints_audio").style.display = "none";
}

async function requestBasicPermissions(constraint = { video: true, audio: true }, callback = setupWebcamSelection, miconly = false) {
	if (session.taintedSession === null) {
		log("STILL WAITING ON HASH TO VALIDATE");
		setTimeout(
			function (constraint, callback, miconly) {
				requestBasicPermissions(constraint, callback, miconly);
			},
			1000,
			constraint,
			callback,
			miconly
		);
		return null;
	} else if (session.taintedSession === true) {
		warnlog("HASH FAILED; PASSWORD NOT VALID");
		return false;
	} else {
		log("NOT TAINTED 1");
	}
	setTimeout(function () {
		getById("getPermissions").style.display = "none";
		getById("gowebcam").style.display = "";
	}, 0);
	log("REQUESTING BASIC PERMISSIONS");

	try {
		
		if (!navigator.mediaDevices){
			throw new Error("navigator.mediaDevices not found - check your security / browser settings.");
		}
		
		var timerBasicCheck = null;
		if (!session.cleanOutput) {
			log("Setting Timer for getUserMedia");
			timerBasicCheck = setTimeout(function () {
				if (!session.cleanOutput) {
					if (session.mobile) {
						warnUser("Notice: Camera timed out\n\nDid you accept the camera permissions?\n\nThis error may also appear if you are in a phone call or another app is already using the camera or microphone.");
					} else {
						warnUser("Camera Access Request Timed Out\n\nDid you accept camera permissions? Please do so first.\n\nIf you have NDI Tools installed, try uninstalling that.\n\nPlease also ensure that your camera and audio devices are correctly connected and not already in use. Bypassing USB hubs or using different USB cables can sometimes help.\n\nYou may also just need to restart the computer");
					}
				}
			}, 10000);
		}
		
		let modifiedConstraint = { ...constraint };
		
		try {
            const videoPermission = await navigator.permissions.query({ name: "camera" });
            const audioPermission = await navigator.permissions.query({ name: "microphone" });

            // If video is denied but audio is allowed, adjust the constraint
            if (videoPermission.state === "denied" && constraint.video) {
                warnlog("Video permissions are denied");
                if (constraint.audio) {
                    // Keep audio if it was originally requested
                    modifiedConstraint.video = false;
                } else {
                    // If no audio was requested, this will likely fail
                    throw new Error("Video permissions denied");
                }
            }

            // If audio is denied but video is allowed, adjust the constraint
            if (audioPermission.state === "denied" && constraint.audio) {
                warnlog("Audio permissions are denied");
                if (constraint.video) {
                    // Keep video if it was originally requested
                    modifiedConstraint.audio = false;
                } else {
                    // If no video was requested, this will likely fail
                    throw new Error("Audio permissions denied");
                }
            }
        } catch (permissionError) {
            log("Permissions API check failed:", permissionError);
        }

		if (session.audioInputChannels) {
			if (modifiedConstraint.audio === true) {
				modifiedConstraint.audio = {};
				modifiedConstraint.audio.channelCount = session.audioInputChannels;
			} else if (modifiedConstraint.audio) {
				modifiedConstraint.audio.channelCount = session.audioInputChannels;
			}
		}

		if (session.micSampleRate) {
			if (modifiedConstraint.audio === true) {
				modifiedConstraint.audio = {};
				modifiedConstraint.audio.sampleRate = parseInt(session.micSampleRate);
			} else if (modifiedConstraint.audio) {
				modifiedConstraint.audio.sampleRate = parseInt(session.micSampleRate);
			}
		}
		if (session.micSampleSize) {
			if (modifiedConstraint.audio === true) {
				modifiedConstraint.audio = {};
				modifiedConstraint.audio.sampleSize = parseInt(session.micSampleSize);
			} else if (modifiedConstraint.audio) {
				modifiedConstraint.audio.sampleSize = parseInt(session.micSampleSize);
			}
		}
		
		if (!modifiedConstraint.audio && !modifiedConstraint.video) {
            userWarn("No media types available for request.\n\nPlease ensure you have granted the microphone and camera permissions.");
            return null;
        }

		if (session.safemode) {
			if (modifiedConstraint.video) {
				modifiedConstraint.video = true;
			}
			if (modifiedConstraint.audio) {
				modifiedConstraint.audio = true;
			}
		}
		getUserMediaRequestID += 1;
		var gumID = getUserMediaRequestID;
		log("CONSTRAINT");
		log(modifiedConstraint);
		var timeoutStart = 0;
		if (Firefox) {
			timeoutStart = 500;
		}
		log("timeoutStart :" + timeoutStart);
		setTimeout(
			async function (gumID, constraint, timerBasicCheck, callback, miconly) {
				log("gumID: " + gumID);
				log(constraint);
				var removeAudio = false;
				if (!constraint.audio && !constraint.video) {
					constraint.audio = true;
					removeAudio = true;
				}

				// Permissions API is not supported in all browsers, so we use a try-catch block
				let videoPermission = "prompt";
				let audioPermission = "prompt";

				if (Firefox && Firefox>=132){
					console.warn("😱 see: https://bugzilla.mozilla.org/show_bug.cgi?id=1924572#c1");
				} else {
					try {
						const videoStatus = await navigator.permissions.query({ name: "camera" });
						videoPermission = videoStatus.state;
						const audioStatus = await navigator.permissions.query({ name: "microphone" });
						audioPermission = audioStatus.state;
						log("audioPermission: "+audioPermission);
					} catch (e) {
						warnlog("Permissions API is not fully supported in this browser.");
					}

					if (videoPermission === "granted") {
						constraint.video = false;
					}
					if (audioPermission === "granted") {
						constraint.audio = false;
					}
				}

				if (!constraint.audio && !constraint.video) {
					warnlog("bypassing navigator.mediaDevices.getUserMedia; permissions granted already?");
					clearTimeout(timerBasicCheck);
					if (getUserMediaRequestID !== gumID) {
						warnlog("GET USER MEDIA CALL HAS EXPIRED 3a");
						return;
					}
					closeModal();
					if (callback) {
						callback(miconly);
					}
					return;
				}
				
				if (Firefox){
					constraint = toFirefoxConstraint(constraint);
				}

				warnlog("navigator.mediaDevices.getUserMedia starting...");
				navigator.mediaDevices
					.getUserMedia(constraint)
					.then(function (stream) {
						// Apple needs thi to happen before I can access EnumerateDevices.

						if (removeAudio) {
							constraint.audio = false; // this seeems pointless?
							stream.getTracks().forEach(function (track) {
								stream.removeTrack(track);
								track.stop();
								log("stopping old track");
							});
						}

						log("got first stream");
						clearTimeout(timerBasicCheck);
						if (getUserMediaRequestID !== gumID) {
							warnlog("GET USER MEDIA CALL HAS EXPIRED 3");
							stream.getTracks().forEach(function (track) {
								stream.removeTrack(track);
								track.stop();
								log("stopping old track");
							});
							return;
						}
						closeModal();

						log(stream.getTracks());

						session.streamSrc = stream;
						checkBasicStreamsExist();
						updateRenderOutpipe();

						if (callback) {
							callback(miconly);
						}
					})
					.catch(function (err) {
						clearTimeout(timerBasicCheck);
						warnlog("some error with GetUSERMEDIA");
						console.warn(err); /* handle the error */
						if (err.name == "NotFoundError" || err.name == "DevicesNotFoundError") {
							//required track is missing
						} else if (err.name == "NotReadableError" || err.name == "TrackStartError") {
							//webcam or mic are already in use
						} else if (err.name == "OverconstrainedError" || err.name == "ConstraintNotSatisfiedError") {
							//constraints can not be satisfied by avb. devices
						} else if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") {
							//permission denied in browser
							if (isIFrame) {
								console.error('Make sure that this IFRAME has the correct permissions allowed, ie:\n\niframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;geolocation;screen-wake-lock;";');
							}
							if (!session.cleanOutput) {
								setTimeout(function () {
									if (window.obsstudio) {
										warnUser("Permissions denied.\n\nTo access the camera or microphone from within OBS, please refer to:\n<a href='https://docs.vdo.ninja/guides/share-webcam-from-inside-obs'>docs.vdo.ninja/guides/share-webcam-from-inside-obs</a>.", false, false);
									} else if (ChromiumVersion && !session.mobile) {
										warnUser("<h1>Camera/mic permissions denied</h1>\nPlease ensure you have allowed the mic/camera permissions in your browser, such as like:\n\n<img src='./media/permissions_chrome.jpg' style='max-height:50vh;' />\n\nFor further help on how to resolve this issue, please refer to:\n\n<a target='_blank' href='https://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions'>https://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions</a>.", false, false);
									} else if (Firefox && session.mobile) {
										warnUser(
											"<h3>Camera/mic permission denied</h3>\nPlease allow mic/camera access.\n\n\
								If not prompted, go to Settings -> Site permissions -> exceptions (at bottom) -> vdo.ninja, and then manually enable the permissions.\n\n\
								If Firefox still gives you issues, try in incognito mode or a different browser.\
								For further help, please refer to:\n\n<a target='_blank' href='https://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions'>https://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions</a>.",
											false,
											false
										);
									} else {
										warnUser("Permission access to the camera or microphone was denied.\n\nPlease ensure you have allowed the mic/camera permissions in your browser.\n\nFor guides on how to resolve this issue, please refer to:\n\n<a target='_blank' href='https://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions'>https://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions</a>.", false, false);
									}
								}, 1);
							}
							return;
						} else if (err.name == "TypeError" || err.name == "TypeError") {
							//empty constraints object
						} else {
							//permission denied in browser
							if (!session.cleanOutput) {
								setTimeout(
									function (err) {
										warnUser(err);
									},
									1,
									err
								);
							}
						}
						warnlog("trying to list webcam again");

						if (callback) {
							callback(miconly);
						}
					});
			},
			timeoutStart,
			gumID,
			modifiedConstraint,
			timerBasicCheck,
			callback,
			miconly
		);
	} catch (e) {
		console.warn(e);
		if (!session.cleanOutput) {
			if (window.isSecureContext) {
				warnUser("An error has occured when trying to access the webcam or microphone. The reason is not known.");
			} else if (iOS || iPad) {
				warnUser("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported.");
			} else {
				warnUser("Error acessing camera or microphone.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia");
			}
		}
	}
	return null;
}

function awaitInboundCall() {
	if (session.twilio) {
		return;
	}
	loadScript("./thirdparty/twilio.min.js?v1", async function () {
		if (!session.twilio) {
			session.twilio = {};
		}

		class MyAudioProcessor {
			constructor(audioContext) {
				console.log("constructor");
				this.audioContext = session.audioCtxOutbound;
				session.twilio.mixer = session.audioCtxOutbound.createGain(); // This serves as our mixer node
				this.destination = session.twilio.destination = session.audioCtxOutbound.createMediaStreamDestination(); // Destination node
				this.background = this.audioContext.createMediaElementSource(document.createElement("video"));
			}

			async createProcessedStream(stream) {
				const source = this.audioContext.createMediaStreamSource(stream);
				const gain = this.audioContext.createGain();
				gain.gain.value = 0;
				source.connect(gain);
				gain.connect(session.twilio.mixer);
				// session.twilio.micSources.push(stream);

				session.twilio.mixer.connect(this.destination);
				return this.destination.stream;
			}

			async destroyProcessedStream(stream) {
				console.log("destroyProcessedStream called");
				console.log(stream);
			}
		}

		const response = await fetch("https://call.vdo.ninja:8443/token2");
		session.twilio.data = await response.json();

		session.twilio.device = new Twilio.Device(session.twilio.data.token);

		const audioProcessor = new MyAudioProcessor();
		await session.twilio.device.audio.addProcessor(audioProcessor);

		session.twilio.device.register();

		warnUser("Dial in access code: " + session.twilio.data.dialInNumber);

		session.twilio.device.on("registered", function () {
			console.log("Twilio.Device is ready to receive incoming calls!");
		});

		async function refresh() {
			console.log("refreshing token");
			const response = await fetch("https://call.vdo.ninja:8443/refresh");
			session.twilio.data = await response.json();
			session.twilio.device.updateToken(session.twilio.data.token);
		}

		session.twilio.refreshInterval = setInterval(refresh, 50 * 60 * 1000);

		session.twilio.micSources = [];

		session.twilio.updateMixer = async function (UUID = false) {
			try {
				console.log("update mixer");
				session.twilio.micSources.forEach(src => {
					src.disconnect();
				});

				if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getAudioTracks().length) {
					var micSource = session.audioCtxOutbound.createMediaStreamSource(session.videoElement.srcObject);
				} else if (session.streamSrc && session.streamSrc.getAudioTracks().length) {
					var micSource = session.audioCtxOutbound.createMediaStreamSource(session.streamSrc);
				} else {
					var micSource = false;
				}

				for (var uuid in session.rpcs) {
					try {
						if (session.rpcs[uuid].videoElement && session.rpcs[uuid].videoElement.srcObject) {
							var guestSource = session.audioCtxOutbound.createMediaStreamSource(session.rpcs[uuid].videoElement.srcObject);
							if (guestSource) {
								guestSource.connect(session.twilio.mixer);
								session.twilio.micSources.push(guestSource);
								console.log(uuid + " added to twiliio");
							}
						}
					} catch (e) {
						errorlog(e);
					}
				}

				if (micSource) {
					micSource.connect(session.twilio.mixer);
					session.twilio.micSources.push(micSource);
					console.log("micSource added to twiliio");
				}
			} catch (err) {
				console.error("Could not get microphone audio:", err);
			}
		};

		session.twilio.device.on("incoming", call => {
			session.twilio.call = call;
			console.log("Incoming call from: " + session.twilio.call.parameters.From);

			session.twilio.call.on("reject", () => {
				console.log("rejected");
			});
			session.twilio.call.on("disconnect", () => {
				console.log("disconnected");
			});
			session.twilio.call.on("cancel", () => {
				console.log("cancelled");
			});
			session.twilio.call.on("accept", () => {
				console.log("accpted");
			});
			session.twilio.call.on("audio", e => {
				session.twilio.element = e;
				console.log("audio stream available");
				outboundAudioPipeline(session.streamSrc);
			});

			window.focus();
			confirmAlt("Incoming call from: " + session.twilio.call.parameters.From + "\n" + getTranslation("accept-inbound-caller")).then(res => {
				if (res) {
					if (session.audioCtxOutbound.state === "suspended") {
						session.audioCtxOutbound.resume().then(() => {
							console.log("AudioContext resumed");
						});
					}
					session.twilio.updateMixer();
					session.twilio.call.accept();
				} else {
					session.twilio.call.reject();
				}
			});
		});
	});
}

function joinConference(roomid, mute = true) {
	// not used
	loadScript("./thirdparty/twilio.min.js", function () {
		fetch("https://call.vdo.ninja:8443/token")
			.then(response => response.json())
			.then(async data => {
				const device = new Twilio.Device(data.token);
				call = await device.connect({ params: { To: roomid } });
				if (mute) {
					const checkForRemoteStream = setInterval(() => {
						if (call.getRemoteStream && call.getRemoteStream()) {
							if (call.getRemoteStream().getTracks && call.getRemoteStream().getTracks().length) {
								clearInterval(checkForRemoteStream);
								call.getRemoteStream().getTracks()[0].enabled = false;
								console.log("call muted");
							}
						}
					}, 500);
				}
			})
			.catch(error => {
				console.error("Error fetching token: ", error);
			});
	});
}

function listenWebsocket(roomid) {
	// not used
	var callSocket = null;
	var connecting = false;
	var streams = {};
	function connectAudioInbound() {
		clearTimeout(connecting);
		if (callSocket) {
			if (callSocket.readyState === callSocket.OPEN) {
				return;
			}
			try {
				callSocket.close();
			} catch (e) {}
		}
		callSocket = new WebSocket("wss://call.vdo.ninja:8443/" + roomid);
		callSocket.onclose = function () {
			clearTimeout(connecting);
			connecting = setTimeout(function () {
				connectAudioInbound();
			}, 100);
		};
		callSocket.onopen = function () {
			send({ join: true });
		};
		callSocket.addEventListener("message", function (event) {
			var msg = JSON.parse(event.data);
			if (msg.event && msg.event === "media" && msg.media && msg.media.payload && msg.streamSid) {
				playAudioChunk(msg.media.payload, msg.streamSid);
				if (!streams[msg.streamSid]) {
					console.log("stream starting");
					streams[msg.streamSid] = true;
					//audioStreams.innerHTML = "";
					//for (stream in streams){
					//	audioStreams.innerHTML += stream+"<br />";
					//}
					console.log(streams);
				}
			} else if (msg.event && msg.event === "stop" && msg.streamSid) {
				console.log("stream stopping");
				delete streams[msg.streamSid];
				//audioStreams.innerHTML = "";
				//for (stream in streams){
				//	audioStreams.innerHTML += stream+"<br />";
				//}
				console.log(streams);
			} // else {
			//	console.log(msg);
			//	console.log("stream misc");
			//	console.log(streams);
			//}
		});
	}

	function base64ToUint8Array(base64) {
		const binaryString = window.atob(base64);
		const len = binaryString.length;
		const bytes = new Uint8Array(len);
		for (let i = 0; i < len; i++) {
			bytes[i] = binaryString.charCodeAt(i);
		}
		return bytes;
	}
	function muLawToPCM(muLawData, gainFactor = 2.0, threshold = 30000, ratio = 2.0) {
		const pcmData = new Int16Array(muLawData.length);
		for (let i = 0; i < muLawData.length; i++) {
			let muLawByte = muLawData[i];
			muLawByte = ~muLawByte;
			let sign = muLawByte & 0x80;
			muLawByte &= ~0x80;
			let exponent = (muLawByte >> 4) & 0x07;
			let data = muLawByte & 0x0f;
			data |= 0x10;
			data <<= 1;
			data += 1;
			data <<= exponent + 2;
			data -= 0x84;
			if (sign === 0) data = -data;
			let amplifiedData = data * gainFactor;
			if (Math.abs(amplifiedData) > threshold) {
				const overThreshold = Math.abs(amplifiedData) - threshold;
				const compressedData = threshold + overThreshold / ratio;
				amplifiedData = amplifiedData > 0 ? compressedData : -compressedData;
			}
			pcmData[i] = amplifiedData;
		}
		return pcmData;
	}
	function playAudioChunk(base64Chunk, sid) {
		const muLawData = base64ToUint8Array(base64Chunk);
		const pcmData = muLawToPCM(muLawData);
		const audioBuffer = session.audioCtx.createBuffer(1, pcmData.length, 8000);
		const bufferSource = session.audioCtx.createBufferSource();
		const channelData = audioBuffer.getChannelData(0);
		for (let i = 0; i < pcmData.length; i++) {
			channelData[i] = pcmData[i] / 32768.0;
		}
		bufferSource.buffer = audioBuffer;
		bufferSource.connect(session.audioCtx.destination);
		bufferSource.start();
	}
	function send(cmd) {
		callSocket.send(JSON.stringify(cmd));
	}
	connectAudioInbound();
}

function setupWebcamSelection(miconly = false) {
	log("setupWebcamSelection();");

	checkBasicStreamsExist();

	try {
		return enumerateDevices()
			.then(function (dInfo) {
				return gotDevices(dInfo, miconly);
			})
			.then(function () {
				if (getById("webcamquality").elements && parseInt(getById("webcamquality").elements.namedItem("resolution").value) == 3) {
					// this is junk??
					if (session.maxframeRate === false) {
						session.maxframeRate = 30;
						session.maxframeRate_q2 = true;
					}
				} else if (session.maxframeRate_q2) {
					session.maxframeRate = false;
					session.maxframeRate_q2 = false;
				}

				var audioSelect = getById("audioSource");
				var videoSelect = getById("videoSourceSelect");
				var outputSelect = getById("outputSource");

				if (audioSelect.tagName == "UL") {
					audioSelect.onchange = function () {
						if (document.getElementById("gowebcam")) {
							document.getElementById("gowebcam").disabled = true;
							document.getElementById("gowebcam").dataset.audioready = "false";
							//document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD";
							document.getElementById("gowebcam").style.fontWeight = "normal";
							document.getElementById("gowebcam").innerHTML = "Waiting for mic to load";
							miniTranslate(document.getElementById("gowebcam"), "waiting-for-mic-to-load");
						}
						activatedPreview = false;
						grabAudio();
					};
				}
				videoSelect.onchange = function () {
					if (document.getElementById("gowebcam")) {
						document.getElementById("gowebcam").disabled = true;
						document.getElementById("gowebcam").dataset.ready = "false";
						//document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD";
						document.getElementById("gowebcam").style.fontWeight = "normal";
						document.getElementById("gowebcam").innerHTML = "Waiting for Camera to load";
						miniTranslate(document.getElementById("gowebcam"), "waiting-for-camera-to-load");
					}
					warnlog("video source changed");

					activatedPreview = false;
					if (session.quality !== false) {
						grabVideo(session.quality);
					} else if (document.getElementById("webcamquality")) {
						session.quality_wb = parseInt(document.getElementById("webcamquality").elements.namedItem("resolution").value);
						session.quality_room = session.quality_wb;
						grabVideo(session.quality_wb);
					}
				};

				if (Firefox && !session.mobile) {
					outputSelect.onclick = function () {
						log("on click");
						if (outputSelect.options[outputSelect.selectedIndex].value === "others") {
							navigator.mediaDevices.selectAudioOutput().then(device => {
								if (device.kind == "audiooutput") {
									session.sink = device.deviceId;

									try {
										var matched = false;
										outputSelect.childNodes.forEach(ele => {
											if (ele.value === device.deviceId) {
												matched = true;
												ele.selected = true;
											}
										});
										if (!matched) {
											var option = document.createElement("option");
											option.value = device.deviceId;
											option.text = device.label;
											outputSelect.appendChild(option);
											option.selected = true;
										}

										saveSettings(); // we're saving because there was an explicit action to change devices
									} catch (e) {
										errorlog(e);
									}

									if (!session.sink) {
										return;
									} // Not sure this would ever happen, but whatever.

									resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist.
								}
							});
						}
					};
				}

				outputSelect.onchange = function () {
					if (iOS || iPad) {
						return;
					}
					if (Firefox && !session.mobile) {
						if (outputSelect.options[outputSelect.selectedIndex].value === "others") {
							return;
						}
					}

					try {
						session.sink = outputSelect.options[outputSelect.selectedIndex].value;
						saveSettings(); // we're saving because there was an explicit action to change devices
					} catch (e) {
						errorlog(e);
					}

					if (!session.sink) {
						return;
					} // Not sure this would ever happen, but whatever.

					resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist.
				};

				getById("webcamquality").onchange = function () {
					if (document.getElementById("gowebcam")) {
						document.getElementById("gowebcam").disabled = true;
						document.getElementById("gowebcam").dataset.ready = "false";
						//	document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD";
						document.getElementById("gowebcam").style.fontWeight = "normal";
						document.getElementById("gowebcam").innerHTML = "Waiting for Camera to load";
						miniTranslate(document.getElementById("gowebcam"), "waiting-for-camera-to-load");
					}

					if (parseInt(getById("webcamquality").elements.namedItem("resolution").value) == 2) {
						if (session.maxframeRate === false) {
							session.maxframeRate = 30;
							session.maxframeRate_q2 = true;
						}
					} else if (session.maxframeRate_q2) {
						session.maxframeRate = false;
						session.maxframeRate_q2 = false;
					}

					activatedPreview = false;
					session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value);
					session.quality_room = session.quality_wb;
					grabVideo(session.quality_wb);
				};

				if (session.safemode) {
					if (document.getElementById("gowebcam")) {
						document.getElementById("gowebcam").disabled = false;
						//document.getElementById("gowebcam").innerHTML = getTranslation("start");
						miniTranslate(document.getElementById("gowebcam"), "start");
						document.getElementById("gowebcam").dataset.audioready = "true";
						document.getElementById("gowebcam").dataset.ready = "true";
						document.getElementById("gowebcam").focus();
						setTimeout(function () {
							updateForceRotate();
						}, 1000);

						if (session.autostart) {
							publishWebcam(); // no need to mirror as there is no video...
						}
						return;
					}
				}

				if (session.audioDevice !== 0) {
					// change from Auto to Selected Audio Device
					log("SETTING AUDIO DEVICE!!");
					activatedPreview = false;
					grabAudio();
				} else if (document.getElementById("gowebcam")) {
					document.getElementById("gowebcam").dataset.audioready = "true";
				}

				if (session.videoDevice === 0 || miconly) {
					if (session.autostart) {
						publishWebcam(); // no need to mirror as there is no video...
						return;
					} else if (document.getElementById("gowebcam")) {
						document.getElementById("gowebcam").dataset.ready = "true";
						if (document.getElementById("gowebcam").dataset.audioready == "true") {
							document.getElementById("gowebcam").disabled = false;
							miniTranslate(document.getElementById("gowebcam"), "start");
							document.getElementById("gowebcam").focus();
							//document.getElementById("gowebcam").innerHTML = getTranslation("start");
						}
					}
				} else {
					log("GRabbing video: " + session.quality);
					activatedPreview = false;
					if (session.quality !== false) {
						grabVideo(session.quality);
					} else if (document.getElementById("webcamquality")) {
						session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value);
						session.quality_room = session.quality_wb;
						grabVideo(session.quality_wb);
					}
				}

				if (!(iOS || iPad || session.mobile)) {
					try {
						if (outputSelect.selectedIndex >= 0) {
							session.sink = outputSelect.options[outputSelect.selectedIndex].value;
							saveSettings();
							if (session.videoElement && !session.videoElement.paused) {
								resetupAudioOut();
							}
						}
					} catch (e) {
						errorlog(e);
					}
				}
			})
			.catch(e => {
				errorlog(e);
			});
	} catch (e) {
		errorlog(e);
	}
}

Promise.wait = function (ms) {
	return new Promise(function (resolve) {
		setTimeout(resolve, ms);
	});
};

Promise.prototype.timeout = function (ms) {
	return Promise.race([
		this,
		Promise.wait(ms).then(function () {
			if (iOS || iPad) {
				var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nIf using an iPhone or iPad, try fully closing your browser and open it again; Safari sometimes jams up the camera.");
				errormsg.name = "timedOut";
				errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nIf using an iPhone or iPad, try fully closing your browser and open it again; Safari sometimes jams up the camera.";
				throw errormsg;
			} else if (session.mobile) {
				var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nMake sure no other application is using the camera already and that you are using a compatible browser. If issues persist, maybe try the official native mobile app.");
				errormsg.name = "timedOut";
				errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nMake sure no other application is using the camera already and that you are using a compatible browser. If issues persist, maybe try the official native mobile app.";
				throw errormsg;
			} else {
				var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling it.\n\nPlease also ensure your camera and audio device are correctly connected and not already in use. You may also need to refresh the page.");
				errormsg.name = "timedOut";
				errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling it.\n\nPlease also ensure your camera and audio device are correctly connected and not already in use. You may also need to refresh the page.";
				throw errormsg;
			}
		})
	]);
};

async function shareWebsite(autostart = false, evt = false) {
	if (session.iframeSrc) {
		if (!session.cleanOutput) {
			getById("websitesharebutton2").classList.remove("hidden");
		}

		if (evt && (evt.ctrlKey || evt.metaKey)) {
			if (getById("websitesharebutton2").classList.contains("green")) {
				var actionMsg = {};
				actionMsg.infocus = false;

				for (var UUID in session.pcs) {
					if (session.pcs[UUID].allowIframe === true) {
						session.sendMessage(actionMsg, UUID);
					}
				}

				getById("websitesharebutton2").classList.remove("green");
				getById("websitesharebutton2").ariaPressed = "false";
				getById("websitesharebutton2").title = "Hold CTRL (or CMD) and click to spotlight this shared website";
			} else {
				if (session.streamID) {
					var actionMsg = {};
					actionMsg.infocus = session.streamID;

					for (var UUID in session.pcs) {
						if (session.pcs[UUID].allowIframe === true) {
							session.sendMessage(actionMsg, UUID);
						}
					}

					getById("websitesharebutton2").classList.add("green");
					getById("websitesharebutton2").ariaPressed = "true";
					getById("websitesharebutton2").title = "Video is currently spotlighted";
				}
			}
			return;
		}
		getById("websitesharebutton2").classList.remove("green");
		getById("websitesharebutton2").ariaPressed = "false";
		session.iframeSrc = false;

		if (session.director) {
			clearDirectorSettings();
			//setStorage("directorWebsiteShare", {"website":session.iframeSrc, "roomid":session.roomid});
		} else if (document.getElementById("container_iframe") || session.iframeEle) {
			if (session.iframeEle) {
				session.iframeEle.remove();
				session.iframeEle = false;
			}
			if (document.getElementById("container_iframe")) {
				document.getElementById("container_iframe").remove();
			}

			updateMixer();
		}
		getById("websitesharebutton2").classList.add("hidden");
		getById("websitesharebutton").classList.remove("hidden");

		var data = {};
		data.iframeSrc = false;
		for (var UUID in session.pcs) {
			if (session.pcs[UUID].allowIframe === true) {
				session.sendMessage(data, UUID);
			}
		}
		getById("websitesharebutton2").title = "Hold CTRL (or CMD) and click to spotlight this shared website";
		return;
	}
	getById("websitesharebutton2").classList.remove("green");
	getById("websitesharebutton2").ariaPressed = "false";

	if (autostart === false) {
		window.focus();
		var iframeURL = await promptAlt(getTranslation("enter-website"), false, false, session.defaultIframeSrc);
	} else {
		var iframeURL = autostart;
	}
	if (!iframeURL) {
		return;
	}
	if (iframeURL == session.iframeSrc) {
		return;
	}
	session.defaultIframeSrc = iframeURL;

	warnlog(iframeURL);

	session.iframeSrc = parseURL4Iframe(iframeURL);

	if (session.director && !autostart) {
		setStorage("directorWebsiteShare", { website: session.iframeSrc, roomid: session.roomid });
	} else if (session.iframeEle) {
		session.iframeEle.src = session.iframeSrc;
		if (session.iframeSrc.startsWith("https://www.youtube.com/")) {
			// special handler.
			setTimeout(
				function (iframe_id) {
					YoutubeListen(iframe_id);
				},
				1000,
				iframe.id
			);
		}
	} else if (session.iFramesAllowed){
		
		var iframe = document.createElement("iframe");
		iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location
		iframe.src = session.iframeSrc;
		iframe.id = "iframe_source";
		iframe.setAttribute("allowtransparency", "true");		
		iframe.setAttribute("crossorigin", "anonymous");
		iframe.setAttribute("credentialless", "true");
		iframe.loadedYoutubeListen = false;
		session.iframeEle = iframe;

		var container = document.createElement("div");
		iframe.container = container;
		container.id = "container_iframe";

		if (session.iframeSrc.startsWith("https://www.youtube.com/")) {
			// special handler.
			setTimeout(
				function (iframe_id) {
					YoutubeListen(iframe_id);
				},
				1000,
				iframe.id
			);
		}
		updateMixer();
	}

	getById("websitesharebutton2").classList.remove("hidden");
	getById("websitesharebutton").classList.add("hidden");

	var data = {};
	data.iframeSrc = session.iframeSrc;
	for (var UUID in session.pcs) {
		if (session.pcs[UUID].allowIframe === true) {
			session.sendMessage(data, UUID);
		}
	}
}

function screenshareTypeDecider(sstype = 1) {
	if (session.screenshareType) {
		sstype = session.screenshareType;
	}
	if (sstype == 1) {
		toggleScreenShare();
	} else if (sstype == 2) {
		createIframePopup();
	} else if (sstype == 3) {
		createSecondStream();
	}
}

function createScreenShareURL(transparent = true) {
	// iframe.src =
	if (session.screenShareElement) {
		var iFrameID = session.streamID.substring(0, 12) + "_" + session.generateStreamID(5);
	} else if (session.screenshareid) {
		var iFrameID = session.screenshareid;
	} else {
		var iFrameID = session.streamID.substring(0, 12) + "_" + session.generateStreamID(5);
	}

	if (session.exclude) {
		session.exclude.push(iFrameID);
	} else {
		session.exclude = [];
		session.exclude.push(iFrameID);
	}

	var extras = "";
	if (session.password) {
		extras += "&password=" + session.password; // encodeURIComponent(
	}

	if (session.privacy) {
		extras += "&privacy";
	}

	if (session.meshcast) {
		extras += "&meshcast";
	}

	if (session.token) {
		extras += "&token=" + session.token;
	}

	if (session.remote) {
		if (session.remote === true) {
			extras += "&remote";
		} else {
			extras += "&remote=" + session.remote;
		}
	}
	if (session.salt !== location.hostname) {
		// this is default.
		extras += "&salt=" + session.salt;
	}
	if (session.whipOutVideoBitrate) {
		extras += "&wovb=" + session.whipOutVideoBitrate;
	}
	if (session.whipOutScreenShareBitrate) {
		extras += "&wossbitrate=" + session.whipOutScreenShareBitrate;
	}

	if (!session.notifyScreenShare) {
		extras += "&smallshare";
	}

	if (session.screenshareContentHint) {
		extras += "&sshint=" + session.screenshareContentHint;
	} else if (session.contentHint) {
		extras += "&sshint=" + session.contentHint;
	}

	if (session.audioContentHint) {
		extras += "&audiohint=" + session.audioContentHint;
	}

	if (session.whipOutScreenShareCodec) {
		extras += "&whipoutcodec=" + session.whipOutScreenShareCodec;
	} else if (session.whipOutCodec) {
		extras += "&whipoutcodec=" + session.whipOutCodec;
	}

	if (session.screensharequality !== false) {
		extras += "&q=" + session.screensharequality; // &quality works here, since only thing we are doing
	} else if (session.quality !== false) {
		extras += "&q=" + session.quality;
	} else if (session.quality_ss !== false) {
		extras += "&q=" + session.quality_ss;
	} else {
		extras += "&q=0";
	}

	if (session.screenShareLabel !== false) {
		if (session.screenShareLabel) {
			extras += "&label=" + encodeURIComponent(session.screenShareLabel);
		} else if (session.label) {
			extras += "&label=" + encodeURIComponent(session.label);
		}
	}

	if (session.screensharefps !== false) {
		extras += "&maxframeRate=" + parseInt(session.screensharefps * 100) / 100.0;
	}
	if (session.screenshareAEC !== false) {
		extras += "&aec=1";
	}
	if (session.screenshareDenoise !== false) {
		extras += "&denoise=1";
	}
	if (session.screenshareAutogain !== false) {
		extras += "&autogain=1";
	}
	if (session.screenshareStereo !== false) {
		extras += "&stereo=" + session.screenshareStereo;
	}
	if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) {
		extras += "&ar=" + session.forceAspectRatio;
	} else if (session.forceScreenShareAspectRatio) {
		extras += "&ar=" + session.forceScreenShareAspectRatio;
	}
	if (session.muted) {
		extras += "&muted";
	}

	if (session.recordLocal) {
		extras += "&record=" + session.recordLocal;
	}

	if (session.autorecordlocal) {
		extras += "&autorecordlocal";
	}

	if (transparent) {
		extras += "&transparent&cleanish";
	}
	// manual, since I don't want to use the auto-mixer.
	return "?manual&audiodevice=1&screenshare&cb=0&nvb&nsb&autostart&view&room=" + session.roomid + "&push=" + iFrameID + extras;
}

function createIframePopup() {
	if (session.screenShareElement) {
		postMessageIframe(session.screenShareElement, { close: true });
		session.screenShareElement.parentNode.removeChild(session.screenShareElement);
		session.screenShareElement = false;
		updateMixer();
		getById("screenshare2button").classList.remove("green");
		getById("screenshare2button").ariaPressed = "false";
		return;
	}

	if ((session.queue && !session.transferred) || (session.screenShareState && !session.queue && session.transferred)) {
		// if (session.queue || session.transferred){
		//getById("screenshare2button").classList.add("hidden");
		//getById("screensharebutton").classList.remove("hidden");
		toggleScreenShare();
		return;
	} // can't secondary-screen share if in a queue.

	//if (!session.iFramesAllowed){errorlog("Can't create iFRAME - security is tainted due to possible CSS injection");return;} // allow because we are doing &sstype=2; not anything else.
	var iframe = document.createElement("iframe");
	iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location
	iframe.src = "./" + createScreenShareURL();
	iframe.setAttribute("allowtransparency", "true");
	iframe.setAttribute("crossorigin", "anonymous");
	iframe.setAttribute("credentialless", "true");
	iframe.style.width = "100%";
	iframe.style.height = "100%";
	iframe.style.overflow = "hidden";
	iframe.id = "screensharesource";
	iframe.dataset.sid = "#screensharesource";
	iframe.style.zIndex = "0";

	session.screenShareElement = iframe;
	session.screenShareElement.dataset.doNotMove = true;

	document.getElementById("main").appendChild(iframe);

	if (session.screenShareElementHidden) {
		session.screenShareElement.style.display = "none";
	}

	updateMixer();

	getById("screenshare2button").classList.add("green");
	getById("screenshare2button").ariaPressed = "true";

	return; // ignore the rest.
}

function previewWebcam(miconly = false) {
	if (session.taintedSession === null) {
		log("STILL WAITING ON HASH TO VALIDATE");
		setTimeout(
			function (miconly) {
				previewWebcam(miconly);
			},
			1000,
			miconly
		);
		return;
	} else if (session.taintedSession === true) {
		warnlog("HASH FAILED; PASSWORD NOT VALID");
		return;
	} else {
		log("NOT TAINTED");
	}

	if (activatedPreview == true) {
		log("activeated preview return 1");
		return;
	}
	activatedPreview = true;

	if (miconly) {
		// this just shares the preview section with the mic-only and video+mic modes
		if (!getById("add_camera_inner").cloned) {
			getById("add_camera_inner").cloned = true;
			insertAfter(getById("add_camera_inner"), getById("add_microphone"));
			document.getElementById("videoSourceSelect").innerHTML = "";
		}
	} else if (getById("add_camera_inner").cloned) {
		getById("add_camera_inner").cloned = false;
		insertAfter(getById("add_camera_inner"), getById("add_camera"));
		document.getElementById("videoSourceSelect").innerHTML = "";
	}

	if (session.audioDevice === 0) {
		// OFF
		var constraint = {
			audio: false
		};
	} else if ((session.echoCancellation !== false) && (session.autoGainControl !== false) && (session.noiseSuppression !== false) && (session.voiceIsolation !== true)) {
		// AUTO
		var constraint = {
			audio: true
		};
	} else {
		// Disable Echo Cancellation and stuff for the PREVIEW (DEFAULT CAM/MIC)
		var constraint = {
			audio: {}
		};
		if (session.echoCancellation !== false) {
			// if not disabled, we assume it's on
			constraint.audio.echoCancellation = true;
		} else {
			constraint.audio.echoCancellation = false;
			if (!session.cleanoutput) {
				getById("headphoneTip1").classList.remove("hidden");
				miniTranslate(getById("headphoneTipContext1"), "headphones-tip");
				//getById("headphoneTipContext1").innerHTML = getTranslation("headphones-tip");
			}
		}
		if (session.autoGainControl !== false) {
			constraint.audio.autoGainControl = true;
		} else {
			constraint.audio.autoGainControl = false;
		}
		if (session.noiseSuppression !== false) {
			constraint.audio.noiseSuppression = true;
		} else {
			constraint.audio.noiseSuppression = false;
		}
		
		if (session.voiceIsolation===true){
			constraint.audio.voiceIsolation = true;
		}
	}

	if (session.videoDevice === 0 || miconly) {
		constraint.video = false;
	} else {
		constraint.video = true;
	}

	window.onorientationchange = function () {
		if (Firefox) {
			updateForceRotate(true);
		}
		setTimeout(async function () {
			if (session.forceAspectRatio) {
				await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
			}
			if (session.effect && session.effect === "7") {
				digitalZoom();
			}
			updateForceRotate();
		}, 200);
	};

	if (constraint.video === false && constraint.audio === false) {
		if (session.autostart) {
			publishWebcam(false, miconly); // no need to mirror as there is no video...
			return;
		} else {
			getById("getPermissions").style.display = "none";
			if (document.getElementById("gowebcam")) {
				document.getElementById("gowebcam").dataset.ready = "true";
				document.getElementById("gowebcam").dataset.audioready = "true";
				document.getElementById("gowebcam").disabled = false;
				//document.getElementById("gowebcam").innerHTML = getTranslation("start");
				miniTranslate(document.getElementById("gowebcam"), "start");
				document.getElementById("gowebcam").focus();
			}
		}
		return;
	}

	enumerateDevices()
		.then(function (devices) {
			log("enumeratated");
			log(devices);
			var vtrue = false;
			var atrue = false;
			devices.forEach(function (device) {
				if (device.kind === "audioinput") {
					atrue = true;
				} else if (device.kind === "videoinput") {
					vtrue = true;
				}
			});
			if (atrue === false) {
				constraint.audio = false;
			}
			if (vtrue === false) {
				constraint.video = false;
			}
			setTimeout(
				function (constraint, miconly) {
					requestBasicPermissions(constraint, setupWebcamSelection, miconly);
				},
				0,
				constraint,
				miconly
			);
		})
		.catch(error => {
			log("enumeratated failed. Seeking permissions.");
			setTimeout(
				function (constraint, miconly) {
					requestBasicPermissions(constraint, setupWebcamSelection, miconly);
				},
				0,
				constraint,
				miconly
			);
		});
}

function copyFunction(copyText, evt = false) {
	if (evt) {
		if ("buttons" in evt) {
			if (evt.buttons !== 0) {
				return;
			}
		} else if ("which" in evt) {
			if (evt.which !== 0) {
				return;
			}
		}
		popupMessage(evt);
		evt.preventDefault();
		evt.stopPropagation();
	}

	try {
		copyText.select();
		copyText.setSelectionRange(0, 99999);
		document.execCommand("copy");
	} catch (e) {
		var dummy = document.createElement("input");
		document.body.appendChild(dummy);
		dummy.value = copyText;
		dummy.select();
		document.execCommand("copy");
		document.body.removeChild(dummy);
	}
	return false;
}

function generateQRPage() {
	var pass = sanitizePassword(getById("invite_password").value);
	if (pass.length) {
		return generateHash(pass + session.salt, 4)
			.then(function (hash) {
				generateQRPageCallback(hash);
			})
			.catch(errorlog);
	} else {
		generateQRPageCallback("");
	}
}

async function updateLinkWelcome(arg, input) {
	if (input.checked) {
		var response = await promptAlt("Enter the message you'd like the guests to see");
		response = encodeURIComponent(response);
		var param = input.dataset.param.split("=")[0];
		input.dataset.param = param + "=" + response;
	}
	updateLink(arg, input);
}

function updateLinkWebP(arg, input) {
	if (input.checked) {
		if (!(getById("director_block_" + arg).dataset.raw.includes("&broadcast") || getById("director_block_" + arg).dataset.raw.includes("?broadcast"))) {
			getById("broadcastSlider").checked = true;
			updateLink(arg, getById("broadcastSlider"));
		}
	}
	updateLink(arg, input);
}

var soloLinkAppended = "";
function updateLink(arg, input, solo = false) {
	log("updateLink: " + input.dataset.param);
	if (input.checked) {
		getById("director_block_" + arg).dataset.raw += input.dataset.param;

		if (solo) {
			soloLinkAppended += input.dataset.param;
		}

		var string = getById("director_block_" + arg).dataset.raw;

		if (arg == 1 && getById("obfuscate_director_" + arg).checked) {
			string = obfuscateURL(string);
		}

		getById("director_block_" + arg).href = string;
		getById("director_block_" + arg).innerText = string;
	} else {
		var string = getById("director_block_" + arg).dataset.raw + "&";
		string = string.replace(input.dataset.param + "&", "&");
		string = string.substring(0, string.length - 1);
		getById("director_block_" + arg).dataset.raw = string;

		if (solo) {
			soloLinkAppended += "&";
			soloLinkAppended = soloLinkAppended.replace(input.dataset.param + "&", "&");
			soloLinkAppended = soloLinkAppended.substring(0, soloLinkAppended.length - 1);
		}

		if (arg == 1 && getById("obfuscate_director_" + arg).checked) {
			string = obfuscateURL(string);
		}

		// document.querySelector("soloLink")
		// soloLink

		getById("director_block_" + arg).href = string;
		getById("director_block_" + arg).innerText = string;
	}
	if (solo) {
		document.querySelectorAll("a.soloLink").forEach(ele => {
			try {
				var href = ele.getAttribute("value") + soloLinkAppended;
				ele.href = href;
				ele.innerHTML = href;
			} catch (e) {
				errorlog(e);
			}
		});
	}

	saveDirectorSettings();
}

function changeURL(changeURL) {
	window.focus();
	if (session.consent) {
		hangup(false);
		window.location.href = changeURL;
	} else {
		confirmAlt(getTranslation("director-redirect-1") + changeURL + getTranslation("director-redirect-2")).then(res => {
			if (res) {
				hangup(false);
				window.location.href = changeURL;
			}
		});
	}
}

function updateLinkInverse(arg, input) {
	log("updateLinkInverse");
	log(input.dataset.param);
	if (!input.checked) {
		getById("director_block_" + arg).dataset.raw += input.dataset.param;

		var string = getById("director_block_" + arg).dataset.raw;

		if (arg == 1 && getById("obfuscate_director_" + arg).checked) {
			string = obfuscateURL(string);
		}

		getById("director_block_" + arg).href = string;
		getById("director_block_" + arg).innerText = string;
	} else {
		var string = getById("director_block_" + arg).dataset.raw + "&";
		string = string.replace(input.dataset.param + "&", "&");
		string = string.substring(0, string.length - 1);
		getById("director_block_" + arg).dataset.raw = string;

		if (arg == 1 && getById("obfuscate_director_" + arg).checked) {
			string = obfuscateURL(string);
		}

		getById("director_block_" + arg).href = string;
		getById("director_block_" + arg).innerText = string;
	}
}

function updateLinkScene(arg, input) {
	log("updateLinkScene");
	var string = getById("director_block_" + arg).dataset.raw || "";

	if (input.checked) {
		string = changeParam(string, "scene", "0");
	} else {
		string = changeParam(string, "scene", "1");
	}
	getById("director_block_" + arg).dataset.raw = string;

	if (arg == 1 && getById("obfuscate_director_" + arg).checked) {
		string = obfuscateURL(string);
	}

	getById("director_block_" + arg).href = string;
	getById("director_block_" + arg).innerText = string;
}

function fullscreenPageToggle(state = null) {
	try {
		if (!document.fullscreenElement) {
			// not currently full screen
			if (state !== false) {
				// if state is false, we are already not full screen
				if (document.documentElement.requestFullscreen) {
					document.documentElement.requestFullscreen();
				} else if (document.documentElement.webkitRequestFullscreen) {
					document.documentElement.webkitRequestFullscreen();
				}
			}
		} else if (document.exitFullscreen) {
			if (!state) {
				// if toggle mode or state=false
				document.exitFullscreen();
			}
		} else if (document.webkitExitFullscreen) {
			if (!state) {
				// if toggle mode or state=false
				document.webkitExitFullscreen();
			}
		}
		//updateMixer(); // we will do this on the event for this instead
	} catch (e) {
		errorlog(e);
	}
}

session.pipWindow = false;
async function PictureInPicturePageToggle(state = null) {
	try {
		if (typeof documentPictureInPicture === "undefined") {
			return;
		}
		if (session.pipWindow) {
			getById("testtone").parentNode.insertBefore(session.pipWindow.document.getElementById("gridlayout"), getById("testtone"));
			session.pipWindow.close();
			session.pipWindow = null;
			updateMixer();
			getById("PictureInPicturePage").classList.remove("green");
		} else {
			session.pipWindow = await documentPictureInPicture.requestWindow({ width: 614, height: 344 }); // 360 + 30px for the window header

			session.pipWindow.addEventListener("pagehide", event => {
				if (session.pipWindow) {
					getById("testtone").parentNode.insertBefore(session.pipWindow.document.getElementById("gridlayout"), getById("testtone"));
					session.pipWindow.close();
					session.pipWindow = null;
					updateMixer();
					getById("PictureInPicturePage").classList.remove("green");
				}
			});

			var pipWindowHead = '<title>Pop-out Window</title>';
			pipWindowHead += '<link rel="stylesheet" href="./main.css"/>';
			
			session.pipWindow.document.body.className = "main";
			session.pipWindow.document.head.innerHTML = pipWindowHead;
			session.pipWindow.document.body.style = document.body.style;
			session.pipWindow.document.title = "Pop-out Window";
			session.pipWindow.document.body.append(getById("gridlayout"));
			session.pipWindow.onresize = updateMixer;
			updateMixer(); // just in case onresize doesn't trigger
			getById("PictureInPicturePage").classList.add("green");
		}
	} catch (e) {
		errorlog(e);
	}
}

function resetGen() {
	getById("gencontent").style.display = "block";
	getById("gencontent2").style.display = "none";
	getById("gencontent2").className = ""; //container-inner
	getById("gencontent").className = "container-inner"; //
	getById("gencontent2").innerHTML = "";
	getById("videoname4").focus();
}

function generateQRPageCallback(hash) {
	try {
		var title = getById("videoname4").value;
		if (title.length) {
			title = title.replace(/[\W]+/g, "_").replace(/_+/g, "_"); // but not what others might get. TODO: allow for non-alphanumeric characters; santitize, then URL encode instead,
			title = "&label=" + title;
		}
		var sid = session.generateStreamID();

		var viewstr = "";
		var sendstr = "";

		if (getById("invite_bitrate").checked) {
			viewstr += "&bitrate=20000";
		}
		if (getById("invite_vp9").checked) {
			viewstr += "&codec=vp9";
		}
		if (getById("invite_stereo").checked) {
			viewstr += "&stereo";
			sendstr += "&stereo";
		}
		if (getById("invite_automic").checked) {
			sendstr += "&audiodevice=1";
		}
		if (getById("invite_automic").checked) {
			sendstr += "&audiodevice=1";
		}
		if (getById("invite_effects").checked) {
			sendstr += "&effects";
		}

		if (getById("invite_remotecontrol").checked) {
			//
			var remote_gen_id = session.generateStreamID();
			sendstr += "&remote=" + remote_gen_id; // security
			viewstr += "&remote=" + remote_gen_id;
		}

		if (getById("invite_joinroom").value.trim().length) {
			sendstr += "&ssid&room=" + getById("invite_joinroom").value.trim();
			viewstr += "&solo&room=" + getById("invite_joinroom").value.trim();
		}

		if (getById("invite_password").value.trim().length) {
			sendstr += "&hash=" + hash;
			viewstr += "&password=" + sanitizePassword(getById("invite_password").value.trim());
		}

		if (session.token) {
			sendstr += "&token=" + session.token;
			viewstr += "&token=" + session.token;
		}

		if (getById("invite_group_chat_type").value) {
			//  0 is default
			if (getById("invite_group_chat_type").value == 1) {
				// no video
				sendstr += "&novideo";
			} else if (getById("invite_group_chat_type").value == 2) {
				// no view or audio
				sendstr += "&view";
			}
		}

		if (getById("invite_quality").value) {
			if (getById("invite_quality").value == 0) {
				sendstr += "&quality=0";
			} else if (getById("invite_quality").value == 1) {
				sendstr += "&quality=1";
			} else if (getById("invite_quality").value == 2) {
				sendstr += "&quality=2";
			}
		}

		var wss = "";

		if (session.wssSetViaUrl) {
			if (session.customWSS && session.customWSS !== true) {
				wss = "&pie=" + session.customWSS;
			} else if (session.customWSS==true){
				wss = "&wss=" + session.wss;
			} else {
				wss = "&wss2=" + session.wss;
			}
		}

		var hoststr = "";
		if (getById("invite_hostlink").checked) {
			hoststr = "https://" + location.host + location.pathname + "?push=" + sid + "_hostlink" + "&view=" + sid + sendstr + "&bitrate=500" + title + wss;
			sendstr = "https://" + location.host + location.pathname + "?push=" + sid + "&view=" + sid + "_hostlink" + sendstr + "&bitrate=1200" + title + wss;
		} else {
			sendstr = "https://" + location.host + location.pathname + "?push=" + sid + sendstr + title + wss;
		}

		if (getById("invite_obfuscate").checked) {
			sendstr = obfuscateURL(sendstr);
		}

		viewstr = "https://" + location.host + location.pathname + "?view=" + sid + viewstr + title + wss;
		getById("gencontent").style.display = "none";
		getById("gencontent").className = ""; //
		getById("gencontent2").style.display = "block";
		getById("gencontent2").className = "container-inner";

		getById("gencontent2").innerHTML =
			'<br /><div id="qrcode" style="background-color:white;display:inline-block;color:black;max-width:380px;padding:35px 40px 40px 40px;">\
			<h2 style="margin:0 0 8px 0;color:black"  data-translate="invite-link">Guest Invite Link:</h2>\
			<a class="task grabLinks" title="Click to copy guest invite link to clipboard" onclick="copyFunction(this,event)"   \
			style="word-break: break-all;cursor:copy;background-color:#CFC;border: 2px solid black;width:300px;padding:8px;margin:0px;color:#000;"  href="' +
			sendstr +
			'" >' +
			sendstr +
			' <i class="las la-paperclip" style="cursor:pointer"></i></a><br /><br /></div>\
			<br /><br />and don\'t forget the<h2 style="color:black">OBS Browser Source Link:</h2><a class="task grabLinks" title="Click to copy or just Drag the link directly into OBS" data-drag="1" onclick="copyFunction(this,event)"  style="word-break: break-all;margin:0px;cursor:grab;background-color:#FCC;width:380px;padding:10px;border:2px solid black;margin:5px;color:#000;" href="' +
			viewstr +
			'" >' +
			viewstr +
			' <i class="las la-paperclip" style="cursor:pointer"></i></a> \
			';

		if (hoststr) {
			getById("gencontent2").innerHTML += '<br /><br /><h2 style="color:black">Host Chat Link:</h2><a class="task" title="Click to copy"  onclick="copyFunction(this,event)"  style="font-weight: bold;display: inline-flex;word-break: break-all;margin:0px;cursor:grab;background-color:#cce1ff;width:380px;padding:10px;border:2px solid black;margin:5px;color:#000;" href="' + hoststr + '" >' + hoststr + ' <i class="las la-paperclip" style="cursor:pointer"></i></a>';
		}

		getById("gencontent2").innerHTML +=
			'<br /><br />\
			<span data-translate="please-note-invite-ingestion-link">\
				<li>This invite link and OBS ingestion link are reusable.</li>\
				<li>Only one person may use a specific invite at a time.</li>\
				<li>The stream ID can be changed manually to something else; keep it unique and alphanumeric.</li>\
				<li>Nothing is stored server-side; links do not expire, nor is there anything to delete.</li>\
			</span><br /><br />\
			<button onclick="resetGen();" style="font-size:1.2em;paddding:5px;"><i class="las la-redo-alt"></i> Create Another Invite Link</button>';

		var qrcode = new QRCode(getById("qrcode"), {
			width: 300,
			height: 300,
			colorDark: "#000000",
			colorLight: "#FFFFFF",
			useSVG: false
		});
		qrcode.makeCode(sendstr);
		getById("qrcode").title = "";
		setTimeout(function () {
			getById("qrcode").title = "";
			if (getById("qrcode").getElementsByTagName("img").length) {
				getById("qrcode").getElementsByTagName("img")[0].style.cursor = "none";
				getById("qrcode").getElementsByTagName("img")[0].style.margin = "0 auto";
			}
		}, 100); // i really hate the title overlay that the qrcode function makes
	} catch (e) {
		errorlog(e);
	}
}

function initSceneList(UUID) {
	Object.keys(session.sceneList).forEach((scene, index) => {
		if (getById("container_" + UUID).querySelectorAll('[data-scene="' + scene + '"]').length) {
			return;
		} // already exists.
		var newScene = document.createElement("div");
		newScene.innerHTML = '<button style="margin: 0 5px 10px 5px;" data-sid="' + session.rpcs[UUID].streamID + '" data--u-u-i-d="' + UUID + '" data-action-type="addToScene" data-scene="' + scene + '"   title="Add to Scene ' + scene + '" onclick="directEnable(this, event);"><span ><i class="las la-plus-square" style="color:#060"></i> Scene: ' + scene + "</span></button>";
		newScene.classList.add("customScene");

		var added = false;
		getById("container_" + UUID)
			.querySelectorAll(".customScene>[data-scene]")
			.forEach(ele => {
				log(ele);
				if (!added && ele.dataset.scene > scene + "") {
					ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode);
					added = true;
				}
			});
		if (!added) {
			getById("container_" + UUID).appendChild(newScene);
		}
	});
}

function updateSceneList(scene) {
	// custom scenes only.
	if (!session.director) {
		return;
	}
	if (scene in session.sceneList) {
		return;
	}

	if (parseInt(scene) + "" === scene) {
		if (parseInt(scene) >= 0 && parseInt(scene) <= session.maxScene) {
			return;
		}
	}

	session.sceneList[scene] = true;
	for (var UUID in session.rpcs) {
		var newScene = document.createElement("div");
		newScene.innerHTML = '<button style="margin: 0 5px 10px 5px;" data-sid="' + session.rpcs[UUID].streamID + '" data--u-u-i-d="' + UUID + '" data-action-type="addToScene" data-scene="' + scene + '"  title="Add to Scene ' + scene + '" onclick="directEnable(this, event);"><span ><i class="las la-plus-square" style="color:#060"></i> Scene: ' + scene + "</span></button>";
		newScene.classList.add("customScene");
		var added = false;
		getById("container_" + UUID)
			.querySelectorAll(".customScene>[data-scene]")
			.forEach(ele => {
				log(ele);
				if (!added && ele.dataset.scene > scene + "") {
					ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode);
					added = true;
				}
			});
		if (!added) {
			getById("container_" + UUID).appendChild(newScene);
		}
	}

	if (session.showDirector) {
		if (document.getElementById("container_director")) {
			var newScene = document.createElement("div");
			newScene.innerHTML = '<button style="margin: 0 5px 10px 5px;" data-sid="' + session.streamID + '" data-action-type="addToScene" data-scene="' + scene + '"  title="Add to Scene ' + scene + '" onclick="directEnable(this, event);"><span ><i class="las la-plus-square" style="color:#060"></i> Scene: ' + scene + "</span></button>";
			newScene.classList.add("customScene");
			//getById("container_director").appendChild(newScene);

			var added = false;
			getById("container_director")
				.querySelectorAll(".customScene>[data-scene]")
				.forEach(ele => {
					if (!added && ele.dataset.scene > scene + "") {
						ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode);
						added = true;
					}
				});
			if (!added) {
				getById("container_director").appendChild(newScene);
			}
		}
	}
}

var vis = (function () {
	var stateKey,
		eventKey,
		keys = {
			hidden: "visibilitychange",
			webkitHidden: "webkitvisibilitychange",
			mozHidden: "mozvisibilitychange",
			msHidden: "msvisibilitychange"
		};
	for (stateKey in keys) {
		if (stateKey in document) {
			eventKey = keys[stateKey];
			break;
		}
	}
	return function (c) {
		if (c) {
			document.addEventListener(eventKey, c);
			//document.addEventListener("blur", c);
			//document.addEventListener("focus", c);
		}
		return !document[stateKey];
	};
})();

function unPauseVideo(videoEle, update = true) {
	try {
		if (!videoEle) {
			return;
		} else if (!(videoEle.dataset.UUID in session.rpcs)) {
			return;
		} else if (!("prePausedBandwidth" in session.rpcs[videoEle.dataset.UUID])) {
			return;
		} // not paused; useless to have, but might as well
		session.rpcs[videoEle.dataset.UUID].manualBandwidth = false;
		//session.rpcs[videoEle.dataset.UUID].manualAudioBandwidth = false;

		if (session.rpcs[videoEle.dataset.UUID].videoElement) {
			session.rpcs[videoEle.dataset.UUID].videoElement.play();
		}

		delete session.rpcs[videoEle.dataset.UUID].prePausedBandwidth;
		session.requestRateLimit(false, videoEle.dataset.UUID, false); // passing a bitrate of false forces the saved existing bitrate to be requested.
		videoEle.classList.remove("paused");
		videoEle.classList.remove("partialFadeout");
		if (update) {
			updateMixer();
		}
	} catch (e) {
		errorlog(e);
	}
}

function pauseVideo(videoEle, update = true) {
	if (!videoEle) {
		return;
	} else if (!(videoEle.dataset.UUID in session.rpcs)) {
		return;
	}
	session.rpcs[videoEle.dataset.UUID].prePausedBandwidth = session.rpcs[videoEle.dataset.UUID].manualBandwidth; // useless, but whatever
	session.rpcs[videoEle.dataset.UUID].manualBandwidth = 0;

	if (session.rpcs[videoEle.dataset.UUID].videoElement) {
		session.rpcs[videoEle.dataset.UUID].videoElement.pause();
	}
	//session.rpcs[videoEle.dataset.UUID].manualAudioBandwidth = 0;
	session.requestRateLimit(false, videoEle.dataset.UUID, true); // passing a bitrate of false forces the saved existing bitrate to be requested.
	videoEle.classList.add("paused");
	videoEle.classList.add("partialFadeout");
	if (update) {
		updateMixer();
	}
}

(function rightclickmenuthing() {
	// right click menu
	"use strict";
	
	function clickInsideElement(e, value = "menu") {
		var el = e.srcElement || e.target;
		if (el.dataset && value in el.dataset) {
			return el;
		} else {
			while ((el = el.parentNode)) {
				if (el.dataset && value in el.dataset) {
					return el;
				}
			}
		}
		return false;
	}

	function getPosition(event2) {
		var posx = 0;
		var posy = 0;

		if (!event2) {
			var event = window.event;
		}

		if (event2.pageX || event2.pageY) {
			posx = event2.pageX;
			posy = event2.pageY;
		} else if (event2.clientX || event2.clientY) {
			posx = event2.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
			posy = event2.clientY + document.body.scrollTop + document.documentElement.scrollTop;
		}

		return { x: posx, y: posy };
	}

	var taskItemInContext;
	var clickCoordsX;
	var clickCoordsY;
	var menu;
	var menuState = 0;
	var lastMenu = false;
	var menuWidth;
	var menuHeight;
	var windowWidth;
	var windowHeight;

	function contextListener() {
		document.addEventListener("contextmenu", function (e) {
			
			if (!session.cleanish && session.cleanOutput) {
				e.preventDefault();
				e.stopPropagation();
				return;
			}
			
			if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
				if (e && !e.ctrlKey && !e.metaKey) {
					return;
				}
			} else if (e && (e.ctrlKey || e.metaKey)) {
				return;
			} // allow for development ease

			taskItemInContext = clickInsideElement(e, "menu");

			if (taskItemInContext) {
				e.preventDefault();
				e.stopPropagation();
				if (taskItemInContext.dataset && taskItemInContext.dataset.menu) {
					toggleMenuOn(taskItemInContext.dataset.menu, taskItemInContext);
				} else {
					toggleMenuOn();
				}
				positionMenu(e);
				return false;
			} else {
				taskItemInContext = null;
				toggleMenuOff();
			}
		});
	}

	function menuClickListener(e) {
		var clickeElIsLink = clickInsideElement(e, "action");
		if (clickeElIsLink) {
			e.preventDefault();
			e.stopPropagation();
			menuItemListener(clickeElIsLink, false, e);
			return false;
		} else {
			var button = e.which || e.button;
			if (button === 1) {
				toggleMenuOff();
			}
		}
	}

	function handleInputElement(e) {
		// for the input range slider version
		var clickeElIsLink = clickInsideElement(e, "action");
		if (clickeElIsLink) {
			e.preventDefault();
			e.stopPropagation();
			menuItemListener(clickeElIsLink, e.srcElement, e);
			return false;
		} else {
			var button = e.which || e.button;
			if (button === 1) {
				toggleMenuOff();
			}
		}
	}

	function toggleMenuOn(menutype = false, ele = false) {
		if (lastMenu && lastMenu !== menutype) {
			try {
				menuState = 0;
				getById(lastMenu).classList.remove("context-menu--active");

				document.removeEventListener("click", menuClickListener);
				menu.querySelectorAll("input").forEach(ele => {
					ele.removeEventListener("input", handleInputElement);
				});
			} catch (e) {}
		}
		menu = getById(menutype || "context-menu");
		menuItemSyncState(menu);
		if (menuState !== 1) {
			menuState = 1;
			menu.classList.add("context-menu--active");
			document.addEventListener("click", menuClickListener);
			menu.querySelectorAll("input").forEach(ele => {
				ele.addEventListener("input", handleInputElement);
			});
		}

		if (ele && ele.classOptions) {
			menu.classList.add(ele.classOptions);
		}
		lastMenu = menutype || "context-menu";
	}

	function toggleMenuOff() {
		if (menuState !== 0) {
			menuState = 0;
			menu.classList.remove("context-menu--active");

			document.removeEventListener("click", menuClickListener);
			menu.querySelectorAll("input").forEach(ele => {
				ele.removeEventListener("input", handleInputElement);
			});
		}
		lastMenu = false;
	}

	function positionMenu(e) {
		try {
			var clickCoords = getPosition(e);
			clickCoordsX = clickCoords.x;
			clickCoordsY = clickCoords.y;
		} catch(e){
			errorlog(e);
			return;
		}

		menuWidth = menu.offsetWidth + 4;
		menuHeight = menu.offsetHeight + 4;

		windowWidth = window.innerWidth;
		windowHeight = window.innerHeight;

		if (windowWidth - clickCoordsX < menuWidth) {
			menu.style.left = windowWidth - menuWidth + "px";
		} else {
			menu.style.left = clickCoordsX + "px";
		}

		if (windowHeight - clickCoordsY < menuHeight) {
			menu.style.top = windowHeight - menuHeight + "px";
		} else {
			menu.style.top = clickCoordsY + "px";
		}
	}

	async function menuItemListener(link, inputElement = false, e = false) {
		
		if (link.getAttribute("data-action") === "Open") {
			window.open(taskItemInContext.href);
		} else if (link.getAttribute("data-action") === "Copy") {
			copyFunction(taskItemInContext.href);
		} else if (link.getAttribute("data-action") === "Mirror") {
			if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
				session.mirrored = !session.mirrored;
				applyMirror(false);
				log("session.mirrored");
			} else {
				if ("mirror" in taskItemInContext) {
					taskItemInContext.mirror = !taskItemInContext.mirror;
					applyMirrorGuest(taskItemInContext.mirror, taskItemInContext);
				} else {
					taskItemInContext.mirror = true;
					applyMirrorGuest(taskItemInContext.mirror, taskItemInContext);
				}
			}
		} else if (link.getAttribute("data-action") === "Rotate") {
			if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
				session.rotate = ((session.rotate || 0) + 90) % 360;
				if (Firefox && session.mobile){
					updateForceRotate(true);
				} else {
					updateForceRotate(false);
				}
				log("session.rotate");
				setTimeout(function () {
					updateMixer();
				}, 1);
			} else {
				if ("manualRotate" in taskItemInContext) { 
					taskItemInContext.manualRotate = ((taskItemInContext.manualRotate || 0) + 90) % 360;
					taskItemInContext.rotated = taskItemInContext.manualRotate;
				} else {
					taskItemInContext.manualRotate = ((taskItemInContext.rotated || 0 ) + 90) % 360;
					taskItemInContext.rotated =  taskItemInContext.manualRotate;
				}
				setTimeout(function () {
					updateMixer();
				}, 1);
			}
		} else if (link.getAttribute("data-action") === "FullWindow") {
			if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
				session.infocus = true;
			} else {
				session.infocus = taskItemInContext.dataset.UUID;
			}
			updateMixer();
		} else if (link.getAttribute("data-action") === "ShrinkWindow") {
			session.infocus = false;
			updateMixer();
		} else if (link.getAttribute("data-action") === "Pause") {
			pauseVideo(taskItemInContext);
		} else if (link.getAttribute("data-action") === "UnPause") {
			unPauseVideo(taskItemInContext);
		} else if (link.getAttribute("data-action") === "PiP") {
			togglePictureInPicture(taskItemInContext);
		} else if (link.getAttribute("data-action") === "PiP2") {
			PictureInPicturePageToggle();
		} else if (link.getAttribute("data-action") === "Record") {
			if (taskItemInContext.stopWriter || taskItemInContext.recording) {
			} else if (taskItemInContext.startWriter) {
				taskItemInContext.startWriter();
			} else {
				var videoKbps = session.recordDefault;
				if (session.recordLocal !== false) {
					videoKbps = session.recordLocal;
				}
				recordLocalVideo(null, videoKbps, taskItemInContext);
			}
		} else if (link.getAttribute("data-action") === "StopRecording") {
			if (taskItemInContext.stopWriter) {
				taskItemInContext.stopWriter();
			} else if (taskItemInContext.recording) {
				recordLocalVideo("stop", null, taskItemInContext);
			}
		} else if (link.getAttribute("data-action") === "CopyFrameAsImage") {
			copyVideoFrameToClipboard(taskItemInContext, e);
		} else if (link.getAttribute("data-action") === "SaveFrameToDisk") {
			saveVideoFrameToDisk(taskItemInContext, e);
		} else if (link.getAttribute("data-action") === "DrawOnVideo") {
			if (taskItemInContext.clearDrawOnVideo){
				taskItemInContext.clearDrawOnVideo();
				taskItemInContext.clearDrawOnVideo = null;
			} else {
				taskItemInContext.clearDrawOnVideo = drawOnThis(taskItemInContext);
			}
		} else if (link.getAttribute("data-action") === "ChangeBuffer") {
			toggleBufferSettings(taskItemInContext.dataset.UUID);
		} else if (link.getAttribute("data-action") === "Cast") {
			//copyFunction(taskItemInContext.href);
		} else if (link.getAttribute("data-action") === "Controls") {
			
			//getById("main").classList.add("forcecontrols"); // adds an annoying shadow to the bar area
			//taskItemInContext.showControlBar = true;
			//checkVideoControlBar(taskItemInContext);
			//taskItemInContext.controls = false;
			//ele.focus();
			taskItemInContext.removeAttribute("controls");
			taskItemInContext.setAttribute("controls", "");
			taskItemInContext.controls = true;
			
		} else if (link.getAttribute("data-action") === "HideControls") {
			
			//taskItemInContext.showControlBar = false;
			taskItemInContext.controls = false;
			taskItemInContext.removeAttribute("controls");
			
		} else if (link.getAttribute("data-action") === "Edit") {
			//copyFunction(taskItemInContext.href);
			var response = await promptAlt("Please note, manual edits to the URL may conflict with the toggles", false, false, taskItemInContext.href);
			if (response) {
				taskItemInContext.href = response;
				taskItemInContext.dataset.raw = response;
				taskItemInContext.innerHTML = response;
			}
		} else if (link.getAttribute("data-action") === "QRCode") {
			warnUser("Loading QR Code");
			loadQR(function tt(url) {
				getById("alertModalMessage").innerHTML = "";
				var qrcode = new QRCode(getById("alertModalMessage"), {
					width: 300,
					height: 300,
					colorDark: "#000000",
					colorLight: "#FFFFFF",
					useSVG: false
				});
				qrcode.makeCode(url);
				getById("alertModalMessage").title = "";
				setTimeout(function () {
					getById("alertModalMessage").title = "";
					if (getById("alertModalMessage").getElementsByTagName("img").length) {
						getById("alertModalMessage").getElementsByTagName("img")[0].style.cursor = "none";
					}
				}, 100);
			}, taskItemInContext.href);
		} else if (link.getAttribute("data-action") === "ShowStats") {
			if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
				var [menu, innerMenu] = statsMenuCreator();
				menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);
				printMyStats(innerMenu);
			} else if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) {
				var [menu, innerMenu] = statsMenuCreator();
				printViewStats(innerMenu, taskItemInContext.dataset.UUID);
				menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, taskItemInContext.dataset.UUID);
			}
		} else if (link.getAttribute("data-action") === "OutputAudio") {
			enumerateDevices().then(function (deviceInfo) {
				var ele = getById(taskItemInContext.id);

				var deviceListElement = gotDevices3(deviceInfo, ele);
				if (deviceListElement) {
					warnUser("Select the audio playback destination for this media:\n\n");
					getById("alertModalMessage").appendChild(deviceListElement);
				} else {
					warnUser("No output devices available");
				}
			});

			//
		} else if (link.getAttribute("data-action") === "RemoteHangup") {
			if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) {
				var confirmHangup = confirm(getTranslation("confirm-disconnect-user"));
				if (confirmHangup) {
					var msg = {};
					msg.hangup = true;
					msg.remote = session.remote;
					msg = await session.encodeRemote(msg);
					session.sendRequest(msg, taskItemInContext.dataset.UUID);
					pokeIframeAPI("hungup", "remote", taskItemInContext.dataset.UUID);
				}
			}
		} else if (link.getAttribute("data-action") === "RemoteReload") {
			if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) {
				var confirmReload = confirm(getTranslation("confirm-reload-user"));
				if (confirmReload) {
					var msg = {};
					msg.reload = true;
					msg.remote = session.remote;
					msg = await session.encodeRemote(msg);
					session.sendRequest(msg, taskItemInContext.dataset.UUID);
					pokeIframeAPI("reload", "remote", taskItemInContext.dataset.UUID);
				}
			}
		} else if (link.getAttribute("data-action") === "SSNewTab") {
			var URL = "https://" + window.location.hostname + location.pathname + createScreenShareURL(false);
			log(URL);
			window.open(URL, "_blank").focus();
		} else if (link.getAttribute("data-action") === "pip-clock") {
			popOutClock(taskItemInContext.children[0]);
		} else if (link.getAttribute("data-action") === "Publish") {
			var URL = taskItemInContext.href;
			URL += "&clean&chroma=000&ssar=landscape&nosettings&prefercurrenttab&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&whippush&whippushtoken";
			var win = window.open(URL, "targetWindow", "toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720");
			win.focus();
			win.resizeTo(1280, 720);
		} else if (link.getAttribute("data-action") === "RecordWindow") {
			var URL = taskItemInContext.href;
			URL += "&clean&chroma=000&ssar=landscape&nosettings&prefercurrenttab&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&autorecordlocal";
			var win = window.open(URL, "targetWindow", "toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720");
			win.focus();
			win.resizeTo(1280, 720);
		}
		
		if (inputElement === false) {
			log("Task ID - " + taskItemInContext + ", Task action - " + link.getAttribute("data-action"));
			toggleMenuOff();
		}
	}

	function menuItemSyncState(menu) {
		var items = menu.querySelectorAll("[data-action]");
		for (var i = 0; i < items.length; i++) {
			 if (items[i].getAttribute("data-action") === "FullWindow") {
				if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
					if (session.infocus === true) {
						items[i].parentNode.classList.add("hidden");
					} else {
						items[i].parentNode.classList.remove("hidden");
					}
				} else if (taskItemInContext.dataset.UUID === session.infocus) {
					items[i].parentNode.classList.add("hidden");
				} else {
					items[i].parentNode.classList.remove("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "ShrinkWindow") {
				if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
					if (session.infocus === true) {
						items[i].parentNode.classList.remove("hidden");
					} else {
						items[i].parentNode.classList.add("hidden");
					}
				} else if (taskItemInContext.dataset.UUID === session.infocus) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "Pause") {
				if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) {
					if ("prePausedBandwidth" in session.rpcs[taskItemInContext.dataset.UUID]) {
						items[i].parentNode.classList.add("hidden");
					} else {
						items[i].parentNode.classList.remove("hidden");
					}
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "UnPause") {
				if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) {
					if ("prePausedBandwidth" in session.rpcs[taskItemInContext.dataset.UUID]) {
						items[i].parentNode.classList.remove("hidden");
					} else {
						items[i].parentNode.classList.add("hidden");
					}
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "Record") {
				if (taskItemInContext.stopWriter || taskItemInContext.recording) {
					items[i].parentNode.classList.add("hidden");
				} else {
					items[i].parentNode.classList.remove("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "StopRecording") {
				if (taskItemInContext.stopWriter || taskItemInContext.recording) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "CopyFrameAsImage") {
				if (taskItemInContext.srcObject && taskItemInContext.srcObject.getVideoTracks().length) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "SaveFrameToDisk") {
				if (taskItemInContext.srcObject && taskItemInContext.srcObject.getVideoTracks().length) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "Controls") {
				if (taskItemInContext.controls) {
					items[i].parentNode.classList.add("hidden");
				} else {
					items[i].parentNode.classList.remove("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "HideControls") {
				if (taskItemInContext.controls) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "PiP2") {
				if (typeof documentPictureInPicture !== "undefined") {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "RemoteHangup") {
				if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
					items[i].parentNode.classList.add("hidden");
				} else if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "ChangeBuffer") {
				if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
					items[i].parentNode.classList.add("hidden");
				} else if (session.rpcs[taskItemInContext.dataset.UUID]) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "RemoteReload") {
				if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
					items[i].parentNode.classList.add("hidden");
				} else if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "TipRightClick") {
				if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
					items[i].parentNode.classList.add("hidden");
				} else {
					items[i].parentNode.classList.remove("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "Publish") {
				if (taskItemInContext.classList.contains("publish")) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			} else if (items[i].getAttribute("data-action") === "RecordWindow") {
				if (taskItemInContext.classList.contains("publish")) {
					items[i].parentNode.classList.remove("hidden");
				} else {
					items[i].parentNode.classList.add("hidden");
				}
			}
		}
	}
	contextListener();
})();
	

function checkVideoControlBar(ele){ // this is aggressive. Lets not use it unless required.
	if (ele){
		if (ele.showControlBar){
			if (ele.showControlBarInterval){
				clearTimeout(ele.showControlBarInterval);
			}
			ele.focus();
			ele.removeAttribute("controls");
			ele.setAttribute("controls", "");
			ele.focus();
			ele.showControlBarInterval = setTimeout(function(ele){
				checkVideoControlBar(ele);
			}, 100, ele);
		}
	}
}
function gotDevices3(deviceInfos, vid) {
	var audioEle = document.createElement("select");
	log(deviceInfos);
	if (!deviceInfos.length) {
		return false;
	}
	for (let i = 0; i !== deviceInfos.length; ++i) {
		if (deviceInfos[i].kind === "audiooutput") {
			var opt = document.createElement("option");
			opt.innerText = deviceInfos[i].label;
			opt.value = deviceInfos[i].deviceId;
			audioEle.appendChild(opt);
			audioEle.videoTarget = vid;
			if (vid.sinkId) {
				if (vid.sinkId == deviceInfos[i].deviceId) {
					opt.selected = true;
				}
			} else if (vid.manualSink) {
				if (vid.manualSink == deviceInfos[i].deviceId) {
					opt.selected = true;
				}
			} else if (session.sink) {
				if (session.sink == deviceInfos[i].deviceId) {
					opt.selected = true;
				}
			}
		}
	}
	audioEle.onchange = function () {
		vid.manualSink = this.options[this.selectedIndex].value;
		if (this.videoTarget && this.videoTarget.dataset.UUID) {
			session.audioEffects = true;
			updateIncomingAudioElement(this.videoTarget.dataset.UUID);
		}
		resetupAudioOut(this.videoTarget);
	};
	return audioEle;
}

function popupMessage(e, message = "Copied to Clipboard") {
	// right click menu

	var posx = 0;
	var posy = 0;

	if (!e) var e = window.event;

	if (e.pageX || e.pageY) {
		posx = e.pageX;
		posy = e.pageY;
	} else if (e.clientX || e.clientY) {
		posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
		posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
	}

	posx += 10;

	var menu = getById("messagePopup");
	menu.innerHTML = "<center>" + message + "</center>";
	var menuState = 0;
	var menuWidth;
	var menuHeight;
	var menuPosition;
	var menuPositionX;
	var menuPositionY;

	var windowWidth;
	var windowHeight;

	if (menuState !== 1) {
		menuState = 1;
		menu.classList.add("context-menu--active");
	}

	menuWidth = menu.offsetWidth + 4;
	menuHeight = menu.offsetHeight + 4;

	windowWidth = window.innerWidth;
	windowHeight = window.innerHeight;

	if (windowWidth - posx < menuWidth) {
		menu.style.left = windowWidth - menuWidth + "px";
	} else {
		menu.style.left = posx + "px";
	}

	if (windowHeight - posy < menuHeight) {
		menu.style.top = windowHeight - menuHeight + "px";
	} else {
		menu.style.top = posy + "px";
	}

	function toggleMenuOff() {
		if (menuState !== 0) {
			menuState = 0;
			menu.classList.remove("context-menu--active");
		}
	}
	menu.classList.remove("fadeout");

	var showlength = message.length * 50 || 500;

	setTimeout(function () {
		menu.classList.add("fadeout");
	}, showlength);

	setTimeout(function () {
		toggleMenuOff();
	}, showlength + 1000);
}

function timeSince(date) {
	var seconds = Math.floor((new Date() - date) / 1000);

	var interval = seconds / 31536000;

	if (interval > 1) {
		return Math.floor(interval) + " years";
	}
	interval = seconds / 2592000;
	if (interval > 1) {
		return Math.floor(interval) + " months";
	}
	interval = seconds / 86400;
	if (interval > 1) {
		return Math.floor(interval) + " days";
	}
	interval = seconds / 3600;
	if (interval > 1) {
		return Math.floor(interval) + " hours";
	}
	interval = seconds / 60;
	if (interval > 1) {
		return Math.floor(interval) + " minutes";
	}
	return "Seconds ago";
}

var messageList = [];
function sendChatMessage(chatMsg = false, bc = false) {
	// filtered + visual
	var data = {};
	if (chatMsg === false) {
		var msg = document.getElementById("chatInput").value;
	} else {
		var msg = chatMsg;
	}
	//msg = sanitizeChat(msg);
	if (msg == "") {
		return false;
	}

	msg = convertShortcodes(msg);

	var label = "";
	if (session.label) {
		if (session.director) {
			label = "<b><i><span class='chat_name'>" + session.label + "</span>:</i></b> ";
		} else {
			label = "<b><span class='chat_name'>" + session.label + "</span>:</b> ";
		}
	} else if (session.director) {
		label = "<b><i><span class='chat_name'>Director</span>:</i></b> ";
	}

	if (msg.trim() === "/list") {
		var listMsg = null;
		for (var UUID in session.rpcs) {
			if (session.rpcs[UUID].label) {
				listMsg = UUID + ": " + session.rpcs[UUID].label;
			} else if (session.directorList.indexOf(UUID) >= 0) {
				listMsg = UUID + ": Director";
			} else {
				listMsg = UUID + ": Unknown User";
			}
			var data = {};
			data.msg = listMsg;
			data.label = false;
			data.type = "alert";
			data.time = Date.now();
			messageList.push(data);
		}
		for (var UUID in session.pcs) {
			if (UUID in session.rpcs) {
				continue;
			}
			if (session.pcs[UUID].label) {
				listMsg = UUID + "; " + session.pcs[UUID].label;
			} else if (session.directorList.indexOf(UUID) >= 0) {
				listMsg = UUID + "; Director";
			} else {
				listMsg = UUID + "; Unknown User";
			}
			var data = {};
			data.msg = listMsg;
			data.label = false;
			data.type = "alert";
			data.time = Date.now();
			messageList.push(data);
		}
		if (listMsg === null) {
			data.msg = "No other users are connected to you";
			data.label = false;
			data.type = "alert";
			data.time = Date.now();
			messageList.push(data);
		}
	} else if (msg.startsWith("/msg ")) {
		var msg = msg.split("/msg ")[1];
		msg = msg.split(" ");
		uid = msg.shift().toLowerCase();
		msg = msg.join(" ");
		if (msg == "") {
			return false;
		}
		var sent = false;
		for (var UUID in session.rpcs) {
			if (UUID.startsWith(uid)) {
				sendChat(msg, UUID); // send message to peers
				var data = {};
				data.time = Date.now();
				data.msg = sanitizeChat(msg); // this is what the other person should see
				data.label = label;
				data.type = "sent";
				messageList.push(data);
				sent = true;
			} else if (session.rpcs[UUID].label && session.rpcs[UUID].label.toLowerCase().startsWith(uid)) {
				sendChat(msg, UUID); // send message to peers
				var data = {};
				data.time = Date.now();
				data.msg = sanitizeChat(msg); // this is what the other person should see
				data.label = label;
				data.type = "sent";
				messageList.push(data);
				sent = true;
			} else if (session.directorList.indexOf(UUID) >= 0 && "director".startsWith(uid)) {
				sendChat(msg, UUID); // send message to peers
				var data = {};
				data.time = Date.now();
				data.msg = sanitizeChat(msg); // this is what the other person should see
				data.label = label;
				data.type = "sent";
				messageList.push(data);
				sent = true;
			}
		}
		for (var UUID in session.pcs) {
			if (UUID in session.rpcs) {
				continue;
			}
			if (UUID.startsWith(uid)) {
				sendChat(msg, UUID); // send message to peers
				var data = {};
				data.time = Date.now();
				data.msg = sanitizeChat(msg); // this is what the other person should see
				data.label = label;
				data.type = "sent";
				messageList.push(data);
				sent = true;
			} else if (session.pcs[UUID].label && session.pcs[UUID].label.toLowerCase().startsWith(uid)) {
				sendChat(msg, UUID); // send message to peers
				var data = {};
				data.time = Date.now();
				data.msg = sanitizeChat(msg); // this is what the other person should see
				data.label = label;
				data.type = "sent";
				messageList.push(data);
				sent = true;
			} else if (session.directorList.indexOf(UUID) >= 0 && "director".startsWith(uid)) {
				sendChat(msg, UUID); // send message to peers
				var data = {};
				data.time = Date.now();
				data.msg = sanitizeChat(msg); // this is what the other person should see
				data.label = label;
				data.type = "sent";
				messageList.push(data);
				sent = true;
			}
		}
		if (sent == false) {
			var data = {};
			data.msg = "No user found. Message not sent.";
			data.label = false;
			data.type = "alert";
			data.time = Date.now();
			messageList.push(data);
			updateMessages();
			return false;
		}
	} else if (msg.startsWith("/")) {
		data.msg = "Unknown command. Try '/list' or '/msg username message'.";
		data.label = false;
		data.type = "alert";
		data.time = Date.now();
		messageList.push(data);
		updateMessages();
		return false;
	} else if (session.directorChat === true) {
		if (session.directorList.length) {
			for (var i = 0; i < session.directorList.length; i++) {
				sendChat(msg, session.directorList[i]); // send message to peers
			}
			var data = {};
			data.time = Date.now();
			data.msg = sanitizeChat(msg); // this is what the other person should see
			data.label = label;
			data.type = "sent";
			messageList.push(data);
		}
	} else {
		sendChat(msg); // send message to peers
		data.time = Date.now();
		data.msg = sanitizeChat(msg); // this is what the other person should see
		data.label = label;
		data.type = "sent";
		messageList.push(data);
	}
	document.getElementById("chatInput").value = "";

	messageList = messageList.slice(-100);
	if (!bc && session.broadcastChannel !== false) {
		log(session.broadcastChannel);
		session.broadcastChannel.postMessage(data);
	}
	updateMessages();

	if (isIFrame) {
		parent.postMessage(
			{
				chat: data
			},
			session.iframetarget
		);
	}

	var apiBlob = {};
	apiBlob.time = data.time;
	apiBlob.msg = msg;
	apiBlob.label = session.label;
	apiBlob.type = data.type;
	pokeAPI("chat", apiBlob);

	return true;
}

function disableQualityDirector(UUID) {
	// lets revert back to the director's quality settings after viewing the scene
	try {
		var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.add("disable");
			elements[0].ariaPressed = "false";
			elements[0].classList.remove("pressed");
			elements[0].disabled = "true";
			elements[0].title = getTranslation("preview-meshcast-disabled");
		}
		var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.add("disable");
			elements[0].ariaPressed = "false";
			elements[0].classList.remove("pressed");
			elements[0].disabled = "true";
			elements[0].title = getTranslation("preview-meshcast-disabled");
		}
		var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.add("disable");
			elements[0].ariaPressed = "false";
			elements[0].classList.remove("pressed");
			elements[0].disabled = "true";
			elements[0].title = getTranslation("preview-meshcast-disabled");
		}
	} catch (e) {
		errorlog(e);
	}
}

function applyQualityDirector(uuid = false) {
	// lets revert back to the director's quality settings after viewing the scene
	if (uuid) {
		var eles = document.querySelectorAll('#guestFeeds button.pressed[data-action-type="change-quality1"][data--u-u-i-d="' + uuid + '"],#guestFeeds button.pressed[data-action-type="change-quality2"][data--u-u-i-d="' + uuid + '"],#guestFeeds button.pressed[data-action-type="change-quality3"][data--u-u-i-d="' + uuid + '"]');
		eles.forEach(ele => {
			ele.click();
		});
	} else {
		var eles = document.querySelectorAll('#guestFeeds button.pressed[data-action-type="change-quality1"],#guestFeeds button.pressed[data-action-type="change-quality2"],#guestFeeds button.pressed[data-action-type="change-quality3"]');
		eles.forEach(ele => {
			ele.click();
		});
	}
}

function toggleQualityDirector(bitrate, UUID, ele) {
	// ele is specific to the button in the director's room
	var eles = ele.parentNode.childNodes;
	for (var i = 0; i < eles.length; i++) {
		eles[i].className = "";
	}
	ele.classList.add("pressed");
	ele.ariaPressed = "true";
	session.requestRateLimit(bitrate, UUID);
}

var clockOverlayTimer = null;
function zpadTime(number) {
	var output = "" + number;
	while (output.length < 2) {
		output = "0" + output;
	}
	return output;
}
function showClock() {
	getById("overlayClockContainer").classList.remove("hidden");
}
function hideClock() {
	getById("overlayClockContainer").classList.add("hidden");
}
function setClock(initial = false, color = "#000") {
	if (initial !== false) {
		initial = parseInt(initial);
		getById("overlayClockContainer").dataset.initial = initial;
	} else {
		initial = parseInt(getById("overlayClockContainer").dataset.initial);
	}

	if (initial < 0) {
		initial = 0;
	}

	updateClock(initial, color);
}
function stopClock() {
	var clock = document.getElementById("overlayClock");
	//clock.ctx = null;
	//clock.canvas = null;

	//if (document.pictureInPictureElement && clock.video) {
	//	if (document.pictureInPictureElement == clock.video){
	//		document.exitPictureInPicture();
	//		pokeIframeAPI('picture-in-picture', false);
	//	}
	//clock.video.remove;
	//}
	clock.innerHTML = "";
	clearInterval(clockOverlayTimer);
	//setClock();
	updateClock("0", "#444");
}
function pauseClock() {
	clearInterval(clockOverlayTimer);
	var current = Date.now() - parseInt(getById("overlayClockContainer").dataset.timestamp);

	if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) {
		current = parseInt(Math.round(current / 1000));
	} else {
		current = parseInt(getById("overlayClockContainer").dataset.initial) - parseInt(Math.round(current / 1000));
	}

	getById("overlayClockContainer").dataset.current = current;

	updateClock(current, "#00F");
}
function resumeClock() {
	if ("current" in getById("overlayClockContainer").dataset) {
		startClock(parseInt(getById("overlayClockContainer").dataset.current));
	}
}
function startClock(restart = true) {
	clearInterval(clockOverlayTimer);
	if (restart === true) {
		getById("overlayClockContainer").dataset.timestamp = Date.now();
	} else if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) {
		getById("overlayClockContainer").dataset.timestamp = Date.now() - parseInt(getById("overlayClockContainer").dataset.current * 1000);
	} else {
		getById("overlayClockContainer").dataset.timestamp = Date.now() - (parseInt(getById("overlayClockContainer").dataset.initial) * 1000 - parseInt(getById("overlayClockContainer").dataset.current * 1000));
	}
	stepClock();
	var clock = document.getElementById("overlayClock");
	if (clock && clock.video) {
		clock.innerHTML = "";
		clock.appendChild(clock.video);
		clock.video.play();
	}

	clockOverlayTimer = setInterval(function () {
		stepClock();
	}, 999);
}
function stepClock() {
	var current = Date.now() - parseInt(getById("overlayClockContainer").dataset.timestamp);
	if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) {
		current = parseInt(Math.round(current / 1000));
	} else {
		current = parseInt(getById("overlayClockContainer").dataset.initial) - parseInt(Math.round(current / 1000));
	}

	if (session.directorList.length) {
		var msg = {};
		msg.timer = current;
		for (var i = 0; i < session.directorList.length; i++) {
			msg.UUID = session.directorList[i];
			session.sendMessage(msg, msg.UUID);
		}
	}
	if (current < 0 && current % 2) {
		updateClock(0, "#F00");
	} else if (current < 0) {
		updateClock(0, "#000");
	} else {
		updateClock(current, "#000");
	}
}

function updateClock(timeleft, color = "#000") {
	var minutes = Math.floor(timeleft / 60);
	var seconds = timeleft % 60;

	var clock = document.getElementById("overlayClock");
	if (clock.ctx) {
		clock.ctx.beginPath();
		clock.ctx.rect(0, 0, 230, 40);
		clock.ctx.fillStyle = color;
		clock.ctx.fill();
		clock.ctx.fillStyle = "#FFF";
		clock.ctx.textAlign = "center";
		clock.ctx.font = "50px monospace";
		clock.ctx.fillText(zpadTime(minutes) + ":" + zpadTime(seconds), 115, 37);
	} else {
		clock.innerHTML = zpadTime(minutes) + ":" + zpadTime(seconds);
		clock.style.backgroundColor = color + "9";
	}
}
function popOutClock(clock) {
	if (!clock.ctx) {
		var canvas = document.createElement("canvas");
		canvas.width = "230";
		canvas.height = "40";
		var ctx = canvas.getContext("2d");
		clock.canvas = canvas;
		clock.ctx = ctx;

		ctx.beginPath();
		ctx.rect(0, 0, 230, 40);
		ctx.fillStyle = "#000";
		ctx.fill();
		ctx.fillStyle = "#FFF";
		ctx.font = "50px monospace";
		ctx.textAlign = "center";
		ctx.fillText(clock.innerHTML, 115, 37);
		clock.video = document.createElement("video");
		clock.innerHTML = "";
		clock.appendChild(clock.video);
		clock.video.onloadedmetadata = function () {
			togglePictureInPicture(clock.video);
		};
		
		clock.video.srcObject = canvas.captureStream();
		clock.video.play();
		//clock.video.dataset.menu = "context-menu-clock";
	} else {
		clock.innerHTML = "";
		clock.appendChild(clock.video);
		clock.video.play();
		togglePictureInPicture(clock.video);
	}
}

session.popupChat = null
async function createPopoutChat() {
	if (session.popupChat && !session.popupChat.closed) {
		session.popupChat.focus();
		return;
	}

	if (session.broadcastChannelID === false) {
		session.broadcastChannelID = session.generateStreamID(8);
		log(session.broadcastChannelID);

		session.broadcastChannel = new BroadcastChannel(session.broadcastChannelID);
		session.broadcastChannel.onmessage = function (e) {
		  if ("loaded" in e.data) {
			session.broadcastChannel.postMessage({
			  messageList: messageList
			});
		  } else if ("msg" in e.data) {
			sendChatMessage(e.data.msg, true);
		  }
		};

		session.broadcastChannel.onmessageerror = function (e) {
		  errorlog(e);
		};
	}

	let params = {
		broadcastChannelID: session.broadcastChannelID,
		room: session.roomid || false,
		view: session.view_set ? [...session.view_set,session.streamID].join(",") : (session.roomid ? false : session.streamID),
		label: session.label || false,
		password: session.password
	};

	function encrypt(text, key) {
		const textEncoder = new TextEncoder();
		const encodedText = textEncoder.encode(text);
		const encodedKey = textEncoder.encode(key);

		const encrypted = encodedText.map((byte, i) => 
			byte ^ encodedKey[i % encodedKey.length]
		);

		return btoa(String.fromCharCode.apply(null, encrypted));
	}

	async function generateSecureUrl(params) {
		const ENCRYPTION_KEY = 'your32characterlongencryptionkey!!';

		const filteredParams = Object.fromEntries(
		Object.entries(params).filter(([_, v]) => v != null && v !== undefined)
		);
		const paramsString = JSON.stringify(filteredParams);
		const encrypted = encrypt(paramsString, ENCRYPTION_KEY);
		return `./popout.html?id=${session.broadcastChannelID}&data=${encodeURIComponent(encrypted)}`;
	}

	let srcString = await generateSecureUrl(params);
	log(srcString);

	session.popupChat = window.open(srcString, "popup", "width=600,height=480,toolbar=no,menubar=no,resizable=yes");

	session.popupChat.document.body.style.margin = "0";
	session.popupChat.document.body.style.backgroundColor = "#000";
	session.popupChat.document.body.style.padding = "0";
	session.popupChat.document.body.style.overflow = "hidden";
	session.popupChat.document.title = "Chat pop-out";

	const style = session.popupChat.document.createElement('style');
	style.textContent = `
	@keyframes pulse {
	  0% { background-color: #000; }
	  50% { background-color: #333; }
	  100% { background-color: #000; }
	}
	body {
	  animation: pulse 2s ease-in-out infinite;
	}
	`;
	session.popupChat.document.head.appendChild(style);

	return false;
}

function replaceURLs(message) {
	if (!message) return;
	var urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
	return message.replace(urlRegex, function (url) {
		url = url.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/["']/g, ""); // try to sanitize things, just in case.

		var punc = "";
		while (url[url.length - 1] === ".") {
			url = url.slice(0, -1);
			punc += ".";
		}
		while (url[url.length - 1] === ";") {
			url = url.slice(0, -1);
			punc += ";";
		}
		while (url[url.length - 1] === ",") {
			url = url.slice(0, -1);
			punc += ",";
		}
		while (url[url.length - 1] === "!") {
			url = url.slice(0, -1);
			punc += "!";
		}
		while (url[url.length - 1] === ":") {
			url = url.slice(0, -1);
			punc += ":";
		}
		while (url[url.length - 1] === "*") {
			url = url.slice(0, -1);
			punc += "*";
		}
		while (url[url.length - 1] === ")") {
			url = url.slice(0, -1);
			punc += ")";
		}
		while (url[url.length - 1] === "?") {
			url = url.slice(0, -1);
			punc += "?";
		}

		var hyperlink = url;
		if (!hyperlink.match("^https?://")) {
			hyperlink = "http://" + hyperlink;
		}
		if (url.length > 35) {
			url = url.substring(0, 35) + "...";
		}
		return '<a href="' + hyperlink + '" title="Click to open the link in a new tab" target="_blank" rel="noopener noreferrer">' + url + "</a>" + punc;
	});
}

function getChatMessage(msg, label = false, director = false, overlay = false, UUID = false) {
	msg = sanitizeChat(msg); // keep it clean.
	if (msg == "") {
		return;
	}

	data = {};
	data.time = Date.now();

	var apiBlob = {};
	apiBlob.time = data.time;
	apiBlob.msg = msg;
	apiBlob.label = label;
	apiBlob.type = "recv";

	if (UUID !== false) {
		apiBlob.UUID = UUID;
		if (UUID in session.rpcs) {
			apiBlob.streamID = session.rpcs[UUID].streamID || false;
		}
	}

	if (label) {
		label = sanitizeLabel(label);
	}

	data.msg = msg;
	if (label) {
		data.label = label;
		if (director) {
			data.label = "<b><i><span class='chat_director chat_name'>" + data.label + "</span>:</i></b> ";
		} else {
			data.label = "<b><span class='chat_name'>" + data.label + "</span>:</b> ";
		}
		label = "<span class='chat_name'>" + label + "</span>:"; // label+":";
	} else if (director) {
		data.label = "<b><i><span class='chat_director chat_name'>Director</span>:</i></b> ";
		label = "<span class='chat_director chat_name'>Director</span>:";
	} else {
		if (session.director) {
			data.label = "<span class='chat_name'>Someone</span>: ";
		} else {
			data.label = "";
		}
		label = "";
	}
	data.type = "recv";

	if (overlay) {
			if (!(session.cleanOutput && session.cleanish == false)) {
				var textOverlay = getById("overlayMsgs");
				if (textOverlay) {
					if (overlay == 2) {
						// Clear previous persistent messages
						while (textOverlay.querySelector('.persistent-overlay')) {
							textOverlay.removeChild(textOverlay.querySelector('.persistent-overlay'));
						}
						
						var spanOverlay = document.createElement("span");
						spanOverlay.className = 'persistent-overlay';
						spanOverlay.innerHTML = "<b><i>" + label + "</i></b> " + msg + "<br />";
						textOverlay.appendChild(spanOverlay);
						textOverlay.style.display = "block";
					} else {
						var spanOverlay = document.createElement("span");
						spanOverlay.innerHTML = "<b><i>" + label + "</i></b> " + msg + "<br />";
						textOverlay.appendChild(spanOverlay);
						textOverlay.style.display = "block";
						var showtime = msg.length * 200 + 3000;
						if (showtime > 8000) {
							showtime = 8000;
						}
						setTimeout(
							function (ele) {
								try {
									ele.parentNode.removeChild(ele);
								} catch (e) {}
							},
							showtime,
							spanOverlay
						);
					}
				}
			}
		}

	if (isIFrame) {
		parent.postMessage(
			{
				gotChat: data, // deprecated
				chat: data
			},
			session.iframetarget
		);
	}

	pokeAPI("chat", apiBlob);

	if (session.chatbutton === false) {
		return;
	} // messages can still appear as overlays ^

	messageList.push(data);
	messageList = messageList.slice(-100);

	if (session.beepToNotify) {
		playtone();
		showNotification("new message", msg);
	}
	updateMessages();

	if (session.chat == false) {
		getById("chattoggle").className = "las la-comments toggleSize pulsate";
		getById("chatbutton").className = "float";

		if (getById("chatNotification").value) {
			getById("chatNotification").value = getById("chatNotification").value + 1;
		} else {
			getById("chatNotification").value = 1;
		}
		getById("chatNotification").classList.add("notification", "red");
	}

	if (session.broadcastChannel !== false) {
		session.broadcastChannel.postMessage(data); /* send */
	}
}

function rainbow(step, colours) {
	var r, g, b;
	var h = 1 - step / colours;
	var i = ~~(h * 6);
	var f = h * 6 - i;
	var q = 1 - f;
	switch (i % 6) {
		case 0:
			(r = 1), (g = f), (b = 0);
			break;
		case 1:
			(r = q), (g = 1), (b = 0);
			break;
		case 2:
			(r = 0), (g = 1), (b = f);
			break;
		case 3:
			(r = 0), (g = q), (b = 1);
			break;
		case 4:
			(r = f), (g = 0), (b = 1);
			break;
		case 5:
			(r = 1), (g = 0), (b = q);
			break;
	}
	var c = "#" + ("00" + (~~(r * 200 + 35)).toString(16)).slice(-2) + ("00" + (~~(g * 200 + 35)).toString(16)).slice(-2) + ("00" + (~~(b * 200 + 35)).toString(16)).slice(-2);
	return c;
}

function getColorFromName(str, colorseed = false, totalcolors = false) {
	var out = 0,
		len = str.length;
	if (len > 6) {
		len = 6;
	}

	var seed = 26;
	if (colorseed) {
		seed = colorseed || 1;
	}

	for (var pos = 0; pos < len; pos++) {
		out += (str.charCodeAt(pos) - 64) * Math.pow(seed, len - pos - 1);
	}

	var colours = 167772;

	if (totalcolors) {
		colours = totalcolors;
		if (colours > 167772) {
			colours = 167772;
		} else if (colours < 1) {
			colours = 1;
		}
	}

	out = parseInt(out % colours); // get modulus

	if (colours === 1) {
		return "#F00";
	} else if (colours === 2) {
		switch (out) {
			case 0:
				return "#F00";
			case 1:
				return "#00ABFA";
		}
	} else if (colours === 3) {
		switch (out) {
			case 0:
				return "#F00";
			case 1:
				return "#00A800";
			case 2:
				return "#00ABFA";
		}
	} else if (colours === 4) {
		switch (out) {
			case 0:
				return "#F00";
			case 1:
				return "#FFA500";
			case 2:
				return "#00A800";
			case 3:
				return "#00ABFA";
		}
	} else if (colours === 5) {
		switch (out) {
			case 0:
				return "#F00";
			case 1:
				return "#FFA500";
			case 2:
				return "#00A800";
			case 3:
				return "#00ABFA";
			case 4:
				return "#FF39C5";
		}
	} else {
		out = rainbow(out, colours);
	}
	return out;
}

function updateClosedCaptions(msg, label, UUID) {
	
	if (!session.rpcs[UUID].color && session.ccColored) {
		session.rpcs[UUID].color = getColorFromName(UUID);
	}
	
	msg.counter = parseInt(msg.counter);
	var temp = document.createElement("div");
	temp.innerText = msg.transcript;
	temp.innerText = temp.innerHTML;
	var transcript = temp.textContent || temp.innerText || "";

	if (transcript == "") {
		return;
	}

	transcript = transcript.charAt(0).toUpperCase() + transcript.slice(1);
	//transcript = transcript.substr(-1, 5000); // keep it from being too long

	if (session.nocaptionlabels) {
		label = "";
	} else if (label && !(session.view && !session.view_set)) {
		label = sanitizeLabel(label);
		label = "<b>" + label + ":</b> ";
	} else {
		label = "";
	}

	var textOverlay = getById("overlayMsgs");
	if (textOverlay) {
		if (document.getElementById(UUID + "_" + msg.counter)) {
			var spanOverlay = document.getElementById(UUID + "_" + msg.counter);
		} else {
			var spanOverlay = document.createElement("span");
			spanOverlay.id = UUID + "_" + msg.counter;
			textOverlay.appendChild(spanOverlay);
			textOverlay.style.height = "unset";
			textOverlay.style.textAlign = "left";
			textOverlay.style.display = "block";
			textOverlay.style.position = "fixed";
			textOverlay.style.bottom = "0";
		}

		spanOverlay.innerHTML = label + transcript + "<br />";
		spanOverlay.style.fontSize = (parseInt(session.labelsize || 100) / 100.0) * 4.5 + "vh";
		spanOverlay.style.lineHeight = (parseInt(session.labelsize || 100) / 100) * 6 + "vh";
		spanOverlay.style.margin = (parseInt(session.labelsize || 100) / 100.0) * 0.75 + "vh";

		if (session.rpcs[UUID].color && session.ccColored) {
			spanOverlay.style.color = session.rpcs[UUID].color;
		}

		if (msg.isFinal) {
			var showtime = 3000;
			clearTimeout(spanOverlay.timeout);
			spanOverlay.timeout = setTimeout(
				function (ele) {
					ele.parentNode.removeChild(ele);
				},
				showtime,
				spanOverlay
			);
		} else {
			clearTimeout(spanOverlay.timeout);
			spanOverlay.timeout = setTimeout(
				function (ele) {
					ele.parentNode.removeChild(ele);
				},
				30000,
				spanOverlay
			);
		}
	}
}

var chatUpdateTimeout = null;
function updateMessages() {
	if (session.chatbutton === false) {
		return;
	}
	
	getById("chatNotification").classList.remove("notification", "red");
	
	const chatBody = document.getElementById("chatBody");
	chatBody.innerHTML = "";
	for (var i in messageList) {
		var time = timeSince(messageList[i].time) || "";
		time = "<span style='user-select: none;'> - " + time + "</span>";
		var msg = document.createElement("div");
		var message = replaceURLs(messageList[i].msg);

		if (messageList[i].type == "sent") {
			msg.innerHTML = message + "<i><small><small>" + time + "</small></small></i>";
			msg.classList.add("outMessage");
		} else if (messageList[i].type == "recv" || messageList[i].type == "action") {
			var label = "";
			if (messageList[i].label) {
				label = messageList[i].label;
			}
			msg.innerHTML = label + message + "<i><small><small>" + time + "</small></small></i>";
			msg.classList.add("inMessage");
		} else if (messageList[i].type == "alert") {
			msg.innerHTML = message + "<i><small><small>" + time + "</small></small></i>";
			msg.classList.add("inMessage");
		} else {
			msg.innerHTML = message;
			msg.classList.add("outMessage");
		}

		chatBody.appendChild(msg);
	}
	showDownloadLinks();
	for (var i in msgTransferList) {
		var time = timeSince(msgTransferList[i].time) || "";
		time = "<span style='user-select: none;'> - " + time + "</span>";

		var msg = document.createElement("div");
		if ("idx" in msgTransferList[i]) {
			msg.id = "transfer_" + msgTransferList[i].idx;
			msg.classList.add("transfer");
		}
		if (msgTransferList[i].type == "sent") {
			msg.innerHTML = msgTransferList[i].msg + "<i><small><small>" + time + "</small></small></i>";
			msg.classList.add("outMessage");
		} else if (msgTransferList[i].type == "recv" || msgTransferList[i].type == "action") {
			var label = "";
			if (msgTransferList[i].label) {
				label = msgTransferList[i].label;
			}
			msg.innerHTML = label + msgTransferList[i].msg + "<i><small><small>" + time + "</small></small></i>";
			msg.classList.add("inMessage");
		} else if (msgTransferList[i].type == "alert") {
			msg.innerHTML = msgTransferList[i].msg + "<i><small><small>" + time + "</small></small></i>";
			msg.classList.add("inMessage");
		} else {
			msg.innerHTML = msgTransferList[i].msg;
			msg.classList.add("outMessage");
		}

		if (msg.id && document.getElementById(msg.id)) {
			document.getElementById(msg.id).innerHTML = msg.innerHTML;
		} else {
			chatBody.appendChild(msg);
		}
	}
	if (chatUpdateTimeout) {
		clearInterval(chatUpdateTimeout);
	}
    chatBody.scrollTop = chatBody.scrollHeight;
    if (chatUpdateTimeout) {
        clearTimeout(chatUpdateTimeout);
    }
    chatUpdateTimeout = setTimeout(updateMessages, 60000);
}

function EnterButtonChat(event) {
	// Number 13 is the "Enter" key on the keyboard
	var key = event.which || event.keyCode;
	if (key === 13) {
		// Cancel the default action, if needed
		event.preventDefault();
		// Trigger the button element with a click
		sendChatMessage();
	}
}

function showCustomizer(arg, ele) {
	//getById("directorLinksButton").innerHTML='<i class="las la-caret-right"></i><span data-translate="hide-the-links"> LINKS (GUEST INVITES & SCENES)</span>'
	getById("showCustomizerButton1").style.backgroundColor = "";
	getById("showCustomizerButton2").style.backgroundColor = "";
	getById("showCustomizerButton3").style.backgroundColor = "";
	getById("showCustomizerButton4").style.backgroundColor = "";
	getById("showCustomizerButton1").style.boxShadow = "";
	getById("showCustomizerButton2").style.boxShadow = "";
	getById("showCustomizerButton3").style.boxShadow = "";
	getById("showCustomizerButton4").style.boxShadow = "";

	if (getById("customizeLinks" + arg).style.display != "none") {
		getById("customizeLinks").style.display = "none";
		getById("customizeLinks" + arg).style.display = "none";
	} else {
		//directorLinks").style.display="none";
		getById("showCustomizerButton" + arg).style.backgroundColor = "#1e0000";
		getById("showCustomizerButton" + arg).style.boxShadow = "inset 0px 0px 1px #b90000";
		getById("customizeLinks1").style.display = "none";
		getById("customizeLinks3").style.display = "none";
		getById("customizeLinks").style.display = "block";
		getById("customizeLinks" + arg).style.display = "block";
	}
}

function setPTTvalue() {
	var key = "";
	if (PPTHotkey.ctrl) {
		key += "Control";
	}
	if (PPTHotkey.meta) {
		if (key) {
			key += " + ";
		}
		key += "Meta";
	}
	if (PPTHotkey.alt) {
		if (key) {
			key += " + ";
		}
		key += "Alt";
	}

	if (PPTHotkey.key == "Control") {
		//
	} else if (PPTHotkey.key == "Alt") {
		//
	} else if (PPTHotkey.key == "Meta") {
		//
	} else if (PPTHotkey.key !== false) {
		if (key) {
			key += " + ";
		}
		if (PPTHotkey.key === " ") {
			key += "Space";
		} else {
			key += PPTHotkey.key;
		}
	} else if (key && navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
		getById("pptHotKey").title = "Note: Global hot-keys can't simply be Control, Alt, or Meta keys.";
	}
	getById("pptHotKey").value = key;

	try {
		if (window.electronApi && window.electronApi.updatePPT) {
			window.electronApi.updatePPT(PPTHotkey);
		} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
			if (!ipcRenderer) {
				ipcRenderer = require("electron").ipcRenderer;
			}
			if (ipcRenderer) {
				ipcRenderer.send("PPTHotkey", PPTHotkey);
			}
		}
	} catch (e) {
		errorlog(e);
	}
}

var PPTHotkey = getStorage("PPTHotkey") || false;
if (PPTHotkey) {
	setPTTvalue();
}

function setHotKeyAuto(hotkeyInput) {
	PPTHotkey = {
		ctrl: false,
		alt: false,
		meta: false,
		key: false
	};

	var key = "";

	if (hotkeyInput) {
		const modifiers = hotkeyInput.replaceAll(" ", "+").split("+");
		modifiers.forEach(modifier => {
			const trimmedModifier = modifier.trim().toLowerCase();
			if (trimmedModifier === "control") {
				PPTHotkey.ctrl = true;
				key += "Control";
			} else if (trimmedModifier === "ctrl") {
				PPTHotkey.ctrl = true;
				key += "Control";
			} else if (trimmedModifier === "alt") {
				PPTHotkey.alt = true;
				key += "Alt";
			} else if (trimmedModifier === "meta") {
				PPTHotkey.meta = true;
				key += "Meta";
			}
		});
		var lastKey = modifiers.pop().trim();
		PPTHotkey.key = lastKey;

		if (lastKey || lastKey === " " || lastKey === 0) {
			if (key) {
				key += " + ";
			}
			if (lastKey === " ") {
				key += "Space";
			} else {
				key += lastKey;
			}
		}
	} else {
		PPTHotkey.ctrl = true;
		PPTHotkey.key = "m";
		PPTHotkey.meta = true;
		key = "Control + Alt + m";
	}

	setStorage("PPTHotkey", PPTHotkey, 99999);
	getById("pptHotKey").value = key;

	try {
		if (window.electronApi && window.electronApi.updatePPT) {
			window.electronApi.updatePPT(PPTHotkey);
		} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
			if (!ipcRenderer) {
				ipcRenderer = require("electron").ipcRenderer;
			}
			if (ipcRenderer) {
				ipcRenderer.send("PPTHotkey", PPTHotkey);
			}
		}
	} catch (e) {
		errorlog(e);
	}
}

function setHotKey(keyinput = true) {
	if (!keyinput) {
		// clears if false
		getById("pptHotKey").value = "";
		getById("pptHotKey0").value = "";
		PPTHotkey = false;
		removeStorage("PPTHotkey");

		try {
			if (window.electronApi && window.electronApi.updatePPT) {
				window.electronApi.updatePPT(PPTHotkey);
			} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
				if (!ipcRenderer) {
					ipcRenderer = require("electron").ipcRenderer;
				}
				if (ipcRenderer) {
					ipcRenderer.send("PPTHotkey", PPTHotkey);
				}
			}
		} catch (e) {
			errorlog(e);
		}

		return;
	}

	PPTHotkey = {
		ctrl: false,
		alt: false,
		meta: false,
		key: false
	};

	log(event);
	var key = "";
	if (event.ctrlKey) {
		key += "Control";
		PPTHotkey.ctrl = true;
	}
	if (event.metaKey) {
		if (key) {
			key += " + ";
		}
		key += "Meta";
		PPTHotkey.meta = true;
	}
	if (event.altKey) {
		if (key) {
			key += " + ";
		}
		key += "Alt";
		PPTHotkey.alt = true;
	}

	if (event.key == "Control") {
		//
	} else if (event.key == "Alt") {
		//
	} else if (event.key == "Meta") {
		//
	} else if (event.key || event.key === " " || event.key === 0) {
		if (key) {
			key += " + ";
		}
		if (event.key === " ") {
			key += "Space";
		} else {
			key += event.key;
		}
		PPTHotkey.key = event.key;
	}
	setStorage("PPTHotkey", PPTHotkey, 99999);
	event.target.value = key;

	try {
		if (window.electronApi && window.electronApi.updatePPT) {
			window.electronApi.updatePPT(PPTHotkey);
		} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
			if (!ipcRenderer) {
				ipcRenderer = require("electron").ipcRenderer;
			}
			if (ipcRenderer) {
				ipcRenderer.send("PPTHotkey", PPTHotkey);
			}
		}
	} catch (e) {
		errorlog(e);
	}

	getById("pptHotKey").value = event.target.value;
	getById("pptHotKey0").value = event.target.value;

	event.preventDefault();
	event.stopPropagation();
	return false;
}

function setupGoogleDriveUploader(filename = false, sessionUri = false) {
    if (!session.gdrive) {
        session.gdrive = {};
        session.gdrive.accessToken = false;
    }

    var gdrive = {};
    var uploading = false;
    var tokenClient;
    var isInitialized = false;
    var initializationPromise;

    const SCOPES = "https://www.googleapis.com/auth/drive.file";

    var totalChunksRecorded = 0;
    var totalChunksUploaded = 0;
    var currentByte = 0;
    var chunks = new Blob([]);
    var finalized = false;

    gdrive.promise = false;
    gdrive.sessionUri = sessionUri;
    
    // Create an initialization promise to track when everything is ready
    initializationPromise = new Promise((resolve, reject) => {
        // We'll resolve this when the token client is fully initialized
        if (!gdrive.sessionUri) {
            loadScript("https://accounts.google.com/gsi/client", function() {
                log("Google Identity Services loaded");
                initTokenClient();
                resolve();
            });
        } else {
            resolve();
        }
    });

    // Setup the authentication promise
    if (!filename && !sessionUri) {
        var res, rej;
        gdrive.promise = new Promise((resolve, reject) => {
            res = resolve;
            rej = reject;
        });
        gdrive.promise.resolve = res;
        gdrive.promise.reject = rej;
    }

    gdrive.startResumableUpload = async function(fname, retry = true) {
        console.log("startResumableUpload", retry);

        const fileMetadata = { name: fname };

        if (session.GDRIVE_FOLDERNAME) {
            let folderId = null;

            const query = `name = '${session.GDRIVE_FOLDERNAME}' and mimeType = 'application/vnd.google-apps.folder' and 'root' in parents and trashed = false`;
            const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}`;

            const response = await fetch(url, {
                method: "GET",
                headers: {
                    Authorization: "Bearer " + session.gdrive.accessToken
                }
            });

            const result = await response.json();

            if (result.files && result.files.length > 0) {
                folderId = result.files[0].id;
            }

            if (!folderId) {
                log("creating new folder as folder not found.");
                try {
                    const folderMetadata = {
                        name: session.GDRIVE_FOLDERNAME,
                        mimeType: "application/vnd.google-apps.folder"
                    };

                    const createResponse = await fetch("https://www.googleapis.com/drive/v3/files", {
                        method: "POST",
                        headers: {
                            Authorization: "Bearer " + session.gdrive.accessToken,
                            "Content-Type": "application/json"
                        },
                        body: JSON.stringify(folderMetadata)
                    });

                    const createResult = await createResponse.json();
                    folderId = createResult.id;
                } catch (e) {
                    errorlog(e);
                }
            }

            if (folderId) {
                fileMetadata.parents = [folderId];
            }
        }

        const metadata = new Blob([JSON.stringify(fileMetadata)], { type: "application/json" });
        try {
            var response = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable", {
                method: "POST",
                headers: {
                    Authorization: "Bearer " + session.gdrive.accessToken,
                    "Content-Type": "application/json; charset=UTF-8"
                },
                body: metadata
            });
            if (!response.ok) {
                if (!session.cleanOutput) {
                    warnUser("⚠️ Error: Failed to configure the Google Drive upload.");
                }
                throw new Error("Start resumable upload failed: " + response.statusText);
            }
            return response.headers.get("Location"); // This is the session URI for the resumable upload
        } catch (err) {
            errorlog(err);
            try {
                if (retry) {
                    session.gdrive.accessToken = false;
                    var res, rej;
                    gdrive.promise = new Promise((resolve, reject) => {
                        res = resolve;
                        rej = reject;
                    });
                    gdrive.promise.resolve = res;
                    gdrive.promise.reject = rej;
                    filename = false;
                    
                    // Make sure we're initialized before requesting token
                    await gdrive.ensureInitialized();
                    tokenClient.requestAccessToken();
                    await gdrive.promise;

                    if (session.gdrive.accessToken) {
                        return await gdrive.startResumableUpload(fname, false);
                    } else {
                        return false;
                    }
                }
            } catch (err2) {
                errorlog(err2);
                return false;
            }
        }
    };

    function initTokenClient() {
        console.log("Initializing GIS token client");
        tokenClient = google.accounts.oauth2.initTokenClient({
            client_id: session.GDRIVE_CLIENT_ID,
            scope: SCOPES,
            callback: onTokenResponse,
            error_callback: onTokenError
        });
        
        isInitialized = true;
        
        // If we have no promise yet but the user requested access, set one up
        if (!gdrive.promise && !sessionUri && !filename) {
            var res, rej;
            gdrive.promise = new Promise((resolve, reject) => {
                res = resolve;
                rej = reject;
            });
            gdrive.promise.resolve = res;
            gdrive.promise.reject = rej;
        }
        
        // If we have a filename, request token automatically
        if (filename) {
            console.log("Requesting access token for immediate upload");
            setTimeout(() => {
                tokenClient.requestAccessToken();
            }, 500); // Small delay to ensure tokenClient is fully initialized
        }
    }

    function onTokenError(response) {
        console.warn("Token error:", response);
        if (gdrive.promise && gdrive.promise.reject) {
            gdrive.promise.reject(response);
        }
    }

    async function onTokenResponse(tokenResponse) {
        console.log("Token response received", tokenResponse);
        
        if (tokenResponse.error === "popup_closed_by_user" || tokenResponse.error === "access_denied") {
            errorlog("User cancelled the sign-in process.");
            if (gdrive.promise && gdrive.promise.reject) {
                gdrive.promise.reject(new Error("User cancelled authentication"));
            }
        } else if (tokenResponse.error !== undefined) {
            errorlog("Token error: " + tokenResponse.error);
            if (gdrive.promise && gdrive.promise.reject) {
                gdrive.promise.reject(new Error(tokenResponse.error));
            }
        } else {
            // Successfully got access token
            console.log("Access token obtained successfully");
            session.gdrive.accessToken = tokenResponse.access_token;
            
            if (filename) {
                try {
                    gdrive.sessionUri = await gdrive.startResumableUpload(filename);
                    console.log("Session URI:", gdrive.sessionUri);
                    uploadLoop();
                } catch (e) {
                    console.error("Error starting upload:", e);
                    if (gdrive.promise && gdrive.promise.reject) {
                        gdrive.promise.reject(e);
                    }
                    return;
                }
            }
            
            // Always resolve the promise if we got a token successfully
            if (gdrive.promise && gdrive.promise.resolve) {
                console.log("Resolving promise with access token");
                gdrive.promise.resolve(tokenResponse.access_token);
            }
        }
    }

    // Check if initialized and wait if not
    gdrive.ensureInitialized = async function() {
        if (!isInitialized) {
            console.log("Waiting for initialization to complete...");
            await initializationPromise;
            console.log("Initialization complete");
        }
    };

    // Function to manually request access token
    gdrive.requestAccessToken = async function() {
        await gdrive.ensureInitialized();
        
        if (tokenClient) {
            console.log("Manually requesting access token");
            tokenClient.requestAccessToken();
        } else {
            console.error("Token client not initialized");
            if (gdrive.promise && gdrive.promise.reject) {
                gdrive.promise.reject(new Error("Token client not initialized"));
            }
        }
    };

    gdrive.revokeToken = function() {
        if (session.gdrive.accessToken) {
            google.accounts.oauth2.revoke(session.gdrive.accessToken, () => {
                console.log('Access token revoked');
                session.gdrive.accessToken = false;
            });
        }
    };

    /// the following doesn't need to be signed in; just access to the gdrive.sessionUri URL

    gdrive.addChunk = function(chunk) {
        if (chunk && chunks) {
            totalChunksRecorded += chunk.size;
            chunks = new Blob([chunks, chunk], { type: chunk.type });
            if (!session.cleanOutput) {
                getById("progressContainer").classList.remove("hidden");
            }
            updateProgressBar();
        } else if (chunk === false) {
            finalized = true;
        }
        uploadLoop();
    };

    async function uploadLoop() {
        if (uploading || !gdrive.sessionUri) {
            return;
        }
        uploading = true;
        while (chunks && (finalized || chunks.size > 256 * 1024)) {
            if (finalized) {
                var chunk = chunks.slice(0, chunks.size);
                let res = await finalizeUpload(chunk);
                log(res);
                return;
            } else {
                var chunkSize = Math.floor(chunks.size / (256 * 1024)) * (256 * 1024);
                var chunk = chunks.slice(0, chunkSize);
                chunks = chunks.slice(chunkSize);
            }
            currentByte = await uploadChunk(chunk);
        }
        uploading = false;
    }

    async function uploadChunk(chunk) {
        const endByte = currentByte + chunk.size - 1;
        totalChunksUploaded += chunk.size;
        const headers = new Headers({
            "Content-Range": `bytes ${currentByte}-${endByte}/*`
        });
        const response = await fetch(gdrive.sessionUri, {
            method: "PUT",
            headers: headers,
            body: chunk
        });
        if (!response.ok && response.status !== 308) {
            throw new Error(`Failed to upload chunk: ${response.statusText}`);
        }
        updateProgressBar();
        return endByte + 1;
    }

    async function finalizeUpload(chunk) {
        const endByte = currentByte + chunk.size - 1;
        const headers = new Headers({
            "Content-Range": `bytes ${currentByte}-${endByte}/${endByte + 1}`
        });
        const response = await fetch(gdrive.sessionUri, {
            method: "PUT",
            headers: headers,
            body: chunk
        });
        if (chunk) {
            totalChunksUploaded += chunk.size;
        }
        updateProgressBar(2);

        return response.json();
    }

    function updateProgressBar(state = 0) {
        // Implementation unchanged
        if (state == 2) {
            setTimeout(function() {
                if (getById("progressBar").style.width == "100%") {
                    getById("progressContainer").classList.add("hidden");
                }
            }, 1000);
            getById("progressBar").style.width = "100%";
            var msg = {};
            msg.gdrive = { up: parseInt(totalChunksUploaded / 1024), rec: parseInt(totalChunksUploaded / 1024), state: state };
            for (var i = 0; i < session.directorList.length; i++) {
                msg.UUID = session.directorList[i];
                session.sendMessage(msg, msg.UUID);
            }
        } else if (totalChunksRecorded > 0) {
            var progressPercentage = (totalChunksUploaded / (totalChunksRecorded || 1)) * 100;
            var bytesLeft = parseInt((totalChunksRecorded - totalChunksUploaded) / 1024);
            getById("progressBar").style.width = progressPercentage + "%";
            getById("progressBar").innerHTML = "Upload progress to Google Drive: " + progressPercentage.toFixed(2) + "%, with " + convertKilobytes(bytesLeft) + " left";

            var msg = {};
            msg.gdrive = { up: parseInt(totalChunksUploaded / 1024), rec: parseInt(totalChunksRecorded / 1024), state: state };
            for (var i = 0; i < session.directorList.length; i++) {
                msg.UUID = session.directorList[i];
                session.sendMessage(msg, msg.UUID);
            }
        }
    }
    
    return gdrive;
}

function convertKilobytes(kilobytes) {
	const KB_IN_MB = 1024;
	const KB_IN_GB = 1024 * 1024;

	if (kilobytes >= KB_IN_GB) {
		return Math.ceil(kilobytes / KB_IN_GB).toFixed(0) + " GB";
	} else if (kilobytes >= KB_IN_MB) {
		return Math.ceil(kilobytes / KB_IN_MB).toFixed(0) + " MB";
	} else {
		return kilobytes + "KB";
	}
}
function resumeDropbox() {
	var sessionData = localStorage.getItem("dropboxSession");
	if (sessionData) {
		sessionData = JSON.parse(sessionData);
		sessionData.forEach(main => {
			session.dbx
				.filesUploadSessionFinish({ cursor: { session_id: main.result.session_id, offset: main.vdo.offset }, commit: { path: "/" + main.vdo.filename } })
				.then(function (response) {
					console.log(response);
					console.log("File uploaded to Dropbox:", response.result.path_display);
					DBXqueue = [];
					//localStorage.removeItem('dropboxSession');
				})
				.catch(function (error) {
					localStorage.removeItem("dropboxSession");
					console.error("Error uploading file:", error);
					if (!session.cleanOutput) {
						confirmAlt("There was an error finalizing a previous file upload. \n" + (error.error_summary || "") + "\n\nWould you like to keep trying?").then(res => {
							if (!res) {
								localStorage.removeItem("dropboxSession");
							}
						});
					}
				});
		});
	}
}

async function streamVideoToDropbox(filename) {
	if (!session.dbx) {
		if (session.directorUUID) {
			var msg = {};
			msg.dropbox = -2;
			for (var i = 0; i < session.directorList.length; i++) {
				msg.UUID = session.directorList[i];
				session.sendPeers(msg, msg.UUID);
			}
		}
		return;
	}

	return await session.dbx
		.filesUploadSessionStart({ close: false })
		.then(function (main) {
			var sessionId = main.result.session_id;
			var offset = 0;
			var chunkCounter = 0;
			var DBXqueue = [];
			var uploadActive = false;
			var done;

			log(main);
			main.vdo = {};
			main.vdo.filename = filename;
			main.vdo.offset = offset;

			var sessionData = localStorage.getItem("dropboxSession");
			if (sessionData) {
				try {
					sessionData = JSON.parse(sessionData);
				} catch (e) {
					errorlog(e);
					sessionData = [];
				}
				sessionData.push(main);
			} else {
				sessionData = [main];
			}
			localStorage.setItem("dropboxSession", JSON.stringify(sessionData));

			if (session.directorUUID) {
				var msg = {};
				msg.dropbox = -1;
				for (var i = 0; i < session.directorList.length; i++) {
					msg.UUID = session.directorList[i];
					session.sendPeers(msg, msg.UUID);
				}
			}

			var totalChunksRecorded = 0;
			var totalChunksUploaded = 0;

			function updateTotalChunksRecorded() {
				if (!session.cleanOutput) {
					getById("progressContainer").classList.remove("hidden");
				}
				totalChunksRecorded++;
				updateProgressBar();
			}

			function updateTotalChunksUploaded() {
				totalChunksUploaded++;
				updateProgressBar();
			}

			function finishedChunksUploaded() {
				try {
					getById("progressBar").style.width = "100%";
					setTimeout(function () {
						if (getById("progressBar").style.width == "100%") {
							getById("progressContainer").classList.add("hidden");
						}
					}, 1000);
				} catch (e) {
					errorlog(e);
				}
			}

			function updateProgressBar() {
				if (totalChunksRecorded > 0) {
					var progressPercentage = (totalChunksUploaded / (totalChunksRecorded || 1)) * 100;
					getById("progressBar").style.width = progressPercentage + "%";
					getById("progressBar").innerHTML = "Upload progress to Dropbox: " + progressPercentage.toFixed(2) + "%";
				}
			}

			async function uploadChunk(chunk, oldCursor = false) {
				if (!sessionId) {
					errorlog("WHY IS THIS NOT STOPPED?");
					return;
				}
				if (DBXqueue.length && DBXqueue[DBXqueue.length - 1] === false) {
					//  it's done; ignore anything after this.
					console.log("CHUNKS COMPLETED: " + chunkCounter + "/" + (chunkCounter + (DBXqueue.length - 1)));
					return;
				} else {
					DBXqueue.push(chunk);
					updateTotalChunksRecorded();
					console.log("CHUNKS COMPLETED:" + chunkCounter + "/" + (chunkCounter + DBXqueue.length));
				}

				console.log(DBXqueue);

				if (DBXqueue.length > 1) {
					// still uploading
					return;
				}

				if (session.directorUUID) {
					var msg = {};
					msg.dropbox = DBXqueue.length;
					for (var i = 0; i < session.directorList.length; i++) {
						msg.UUID = session.directorList[i];
						session.sendPeers(msg, msg.UUID);
					}
				}

				if (chunk === false) {
					console.log("CHUNKS COMPLETED UPLOADING.. closing dropbox file: " + offset + " " + filename);
					//console.log(oldCursor);
					console.log({ session_id: sessionId, offset: offset });
					session.dbx
						.filesUploadSessionFinish({ cursor: { session_id: sessionId, offset: offset }, commit: { path: "/" + filename } })
						.then(function (response) {
							//console.log(response);

							DBXqueue = [];
							try {
								console.log("File uploaded to Dropbox:", response.result.path_display);
							} catch (e) {
								errorlog(e);
							}

							var sessionData = localStorage.getItem("dropboxSession");
							if (sessionData) {
								// sessionData = [{object1},{object2}]
								try {
									sessionData = JSON.parse(sessionData);
									sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename);
								} catch (e) {
									errorlog(sessionData);
									errorlog(e);
									sessionData = [];
								}
							} else {
								sessionData = [];
							}
							if (sessionData.length) {
								localStorage.setItem("dropboxSession", JSON.stringify(sessionData));
							} else {
								localStorage.removeItem("dropboxSession");
							}
							sessionId = false;

							if (session.directorUUID) {
								var msg = {};
								msg.dropbox = DBXqueue.length;
								for (var i = 0; i < session.directorList.length; i++) {
									msg.UUID = session.directorList[i];
									session.sendPeers(msg, msg.UUID);
								}
							}
							finishedChunksUploaded();
						})
						.catch(function (e) {
							errorlog(e);
							if (e.error_summary) {
								if (!session.cleanOutput) {
									warnUser("Error\n\n" + error_summary);
								}
							}
						});
				} else if (!uploadActive) {
					uploadActive = true;
					while (DBXqueue.length) {
						try {
							if (!sessionId) {
								errorlog("WHY IS THIS NOT STOPPED ?");
								return;
							}

							let cursor = { session_id: sessionId, offset: offset };
							if (DBXqueue[0] === false) {
								console.log("CHUNKS COMPLETED UPLOADING... closing dropbox file: " + offset);

								await session.dbx
									.filesUploadSessionFinish({ cursor: { session_id: sessionId, offset: offset }, commit: { path: "/" + filename } })
									.then(function (response) {
										//console.log(response);
										DBXqueue = [];

										try {
											console.log("File uploaded to Dropbox:", response.result.path_display);
										} catch (e) {
											errorlog(e);
										}

										var sessionData = localStorage.getItem("dropboxSession");
										if (sessionData) {
											// sessionData = [{object1},{object2}]
											try {
												sessionData = JSON.parse(sessionData);
												sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename);
											} catch (e) {
												errorlog(sessionData);
												errorlog(e);
												sessionData = [];
											}
										} else {
											sessionData = [];
										}
										if (sessionData.length) {
											localStorage.setItem("dropboxSession", JSON.stringify(sessionData));
										} else {
											localStorage.removeItem("dropboxSession");
										}

										sessionId = false;

										if (session.directorUUID) {
											var msg = {};
											msg.dropbox = DBXqueue.length;
											for (var i = 0; i < session.directorList.length; i++) {
												msg.UUID = session.directorList[i];
												session.sendPeers(msg, msg.UUID);
											}
										}
										finishedChunksUploaded();
									})
									.catch(function (e) {
										errorlog(e);
										if (e.error_summary) {
											if (!session.cleanOutput) {
												warnUser("Error\n\n" + error_summary);
											}
										}
									});
							} else {
								await session.dbx
									.filesUploadSessionAppendV2({ cursor: cursor, close: false, contents: DBXqueue[0] })
									.then(function () {
										offset += DBXqueue[0].size;
										main.vdo.offset = offset;
										//localStorage.setItem('dropboxSession', JSON.stringify(main));

										var sessionData = localStorage.getItem("dropboxSession");
										if (sessionData) {
											// sessionData = [{object1},{object2}]
											try {
												sessionData = JSON.parse(sessionData);
												sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename);
												sessionData.push(main);
											} catch (e) {
												errorlog(sessionData);
												errorlog(e);
												sessionData = [main];
											}
										} else {
											sessionData = [main];
										}
										localStorage.setItem("dropboxSession", JSON.stringify(sessionData));

										DBXqueue.shift(); // we got it uploaded
										updateTotalChunksUploaded();
										chunkCounter += 1;
										console.log("CHUNKs COMPLETED:" + chunkCounter + "/" + (chunkCounter + DBXqueue.length));
										if (session.directorUUID) {
											var msg = {};
											msg.dropbox = DBXqueue.length;
											for (var i = 0; i < session.directorList.length; i++) {
												msg.UUID = session.directorList[i];
												session.sendPeers(msg, msg.UUID);
											}
										}
									})
									.catch(function (e) {
										errorlog(e);
										if (e.error_summary) {
											if (!session.cleanOutput) {
												warnUser("Error\n\n" + error_summary);
											}
										}
									});
							}
						} catch (e) {
							errorlog(e);
							break;
						}
					}
					uploadActive = false;
				}
			}

			return uploadChunk;
		})
		.catch(function (e) {
			errorlog(e);
			if (!session.cleanOutput) {
				warnUser("Dropbox failed to initialize.\n\nAre you credentials valid? Tokens may expire after a few hours.", 8000);
			}
		});
}

var recordingBitratePromise = false;
var defaultRecordingBitrate = false;
var lastConfiguredRecordingSetup = false;
async function recordVideo(target, event = null, videoKbps = false) {
	// event.currentTarget,this.parentNode.parentNode.dataset.UUID

	if (session.record === false) {
		warnlog("recordings are disabled by decree of thy host magistrate");
	}
	
	if (!target){return;}

	var UUID = target.dataset.UUID;

	if (!UUID) {
		return;
	}

	var video = session.rpcs[UUID].videoElement;

	if (!video) {
		return;
	}

	if (video.stopWriter) {
		video.stopWriter();
		updateLocalRecordButton(UUID, -1);
		return;
	} else if (video.startWriter) {
		await video.startWriter();
		updateLocalRecordButton(UUID, 0);
		return;
	}

	if (event === null) {
		if (defaultRecordingBitrate === null) {
			updateLocalRecordButton(UUID, -1);
			return;
		}
	} else if (event.ctrlKey || event.metaKey) {
		updateLocalRecordButton(UUID, -3);
		Callbacks.push([recordVideo, target, null, false]);
		log("Record Video queued");
		defaultRecordingBitrate = false;
		recordingBitratePromise = false;
		return;
	} else {
		defaultRecordingBitrate = false;
		recordingBitratePromise = false;
	}

	log("Record Video Clicked");
	if ("recording" in video) {
		log("ALREADY RECORDING!");
		updateLocalRecordButton(UUID, -2);
		video.recorder.stop();
		session.requestRateLimit(35, UUID); // 100kbps
		if (session.audiobitrate === false) {
			session.requestAudioRateLimit(-1, UUID);
		}

		var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
		}
		var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.remove("pressed");
			elements[0].ariaPressed = "false";
		}
		var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.remove("pressed");
			elements[0].ariaPressed = "false";
		}
		return;
	} else {
		updateLocalRecordButton(UUID, 0);
		//target.style.backgroundColor = "#FCC";
		//target.innerHTML = "<i style='font-size:110%;' class='las la-file-download'></i> <span data-translate='Download'>Download</span>";
		video.recording = true;
	}

	video.recorder = {};
	
	var configureRecording = {
		bitrate: videoKbps,
		usePCM: (videoKbps===0 || session.pcm) ? true:false,
		audioOnly: (videoKbps!==false && videoKbps<=0) ? true:false
	};

	if (configureRecording.bitrate === false) {
		if (defaultRecordingBitrate == false) {
			configureRecording.bitrate = session.recordDefault;
			
			if (session.recordLocal !== false) {
				configureRecording.bitrate = session.recordLocal;
			} else if (lastConfiguredRecordingSetup!==false){
				configureRecording = lastConfiguredRecordingSetup;
			}
			
			if (session.pcm){
				configureRecording.usePCM = true;
			}

			if (!recordingBitratePromise) {
				window.focus();
				recordingBitratePromise = promptRecordingOptions(getTranslation("press-ok-to-record"), false, configureRecording);
			}
			configureRecording = await recordingBitratePromise;
			if (configureRecording === null) {
				//target.style.backgroundColor = null;
				//target.innerHTML = '<i class="las la-circle"></i><span data-translate="record"> record local</span>';
				updateLocalRecordButton(UUID, -1);
				target.style.backgroundColor = "";

				try {
					clearInterval(video.recorder.writer.interval);
				} catch (e) {}

				delete video.recorder;
				delete video.recording;
				defaultRecordingBitrate = null;
				return;
			}
			
			defaultRecordingBitrate = configureRecording;
			lastConfiguredRecordingSetup = configureRecording;
		} else {
			configureRecording = defaultRecordingBitrate;
		}
	}

	if (configureRecording.audioOnly) {
		if (session.audiobitrate === false) {
			if (configureRecording.usePCM) {
				session.requestAudioRateLimit(session.audiobitratePRO || 128, UUID); // PCM
			} else {
				session.requestAudioRateLimit(configureRecording.bitrate || 32, UUID); // exact? sure. why not.
			}
		}
	} else {
		if (configureRecording.bitrate < 50) {
			configureRecording.bitrate = 50;
		}
		session.requestRateLimit(configureRecording.bitrate, UUID); // 3200kbps transfer bitrate. Less than the recording bitrate, to avoid waste.

		if (configureRecording.bitrate > 4000) {
			if (session.audiobitrate === false) {
				if (session.pcm) {
					session.requestAudioRateLimit(session.audiobitratePRO, UUID);
				} else {
					session.requestAudioRateLimit(128, UUID);
				}
			}
		} else if (configureRecording.bitrate > 2500) {
			if (session.audiobitrate === false) {
				if (session.pcm) {
					session.requestAudioRateLimit(session.audiobitratePRO, UUID);
				} else {
					session.requestAudioRateLimit(80, UUID);
				}
			}
		}
	}
	
	//



	var cancell = false;
	if (typeof video.srcObject === "undefined" || !video.srcObject) {
		return;
	}

	video.recorder.stop = function (restart = false, notify = false) {
		if (session.dbx && video.dropbox && video.dropbox[filename]) {
			video.dropbox[filename](false);
		}
		if (video.gdrive && video.gdrive[filename]) {
			video.gdrive[filename].addChunk(false);
		}

		if (!video.recording) {
			errorlog("ALREADY STOPPED");
			updateLocalRecordButton(UUID, -1);
			return;
		}

		if (notify) {
			if (!session.cleanOutput) {
				warnUser("A local recording has stopped unexpectedly.");
			}
			if (session.beepToNotify) {
				playtone();
			}
			target.classList.remove("shake");
			setTimeout(
				function (target) {
					target.classList.add("shake");
				},
				10,
				target
			);
		}
		
		

		video.recording = false;
		updateLocalRecordButton(UUID, -2);
		try {
			if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) {
				if (video.recorder.mediaRecorder.state !== "inactive" || video.recorder.mediaRecorder.state === "recording") {
					video.recorder.mediaRecorder.stop();
				}
			}
		} catch (e) {
			errorlog(e);
			try {
				video.recorder.mediaRecorder.stop();
			} catch (e1) {
				errorlog(e1);
			}
		}

		session.requestRateLimit(35, UUID); // 100kbps
		if (session.audiobitrate === false) {
			session.requestAudioRateLimit(-1, UUID);
		}
		var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
		}
		var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.remove("pressed");
			elements[0].ariaPressed = "false";
		}
		var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]');
		if (elements[0]) {
			elements[0].classList.remove("pressed");
			elements[0].ariaPressed = "false";
		}

		cancell = true;
		// log('Recorded Blobs: ', recordedBlobs);
		// download();
		setTimeout(
			(writer1, UUID1, video1) => {
				try {
					writer1.close();
				} catch (e) {}
				updateLocalRecordButton(UUID1, -1);
				delete video1.recorder;
				delete video1.recording;
			},
			1200,
			video.recorder.writer,
			UUID,
			video
		);
	};

	const { readable, writable } = new TransformStream({
		transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b)))
	});

	var filext = ".webm";

	let options = {};

	if (!configureRecording.audioOnly) {
		var tryCodec = session.recordingVideoCodec || ""; // Simplified condition to assign tryCodec

		if (tryCodec && MediaRecorder.isTypeSupported("video/webm;codecs=" + tryCodec)) {
			if (!session.cleanOutput) {
				console.log("👍 The browser 'says' it supports " + tryCodec);
			}
			options.mimeType = "video/webm;codecs=" + tryCodec;

			if (session.pcm) {
				// Fixed the format of the MIME type string
				var mimeTypeWithPCM = "video/webm;codecs=" + tryCodec + ",pcm";
				if (MediaRecorder.isTypeSupported(mimeTypeWithPCM)) {
					options.mimeType = mimeTypeWithPCM;
				} else {
					options.mimeType = "video/webm;codecs=pcm";
				}
			}
		} else {
			// Simplified conditions for PCM support
			if (tryCodec) {
				warnlog("video/webm;codecs=" + tryCodec + " - is not supported");
			}
			options.mimeType = session.pcm && MediaRecorder.isTypeSupported("video/webm;codecs=pcm") ? "video/webm;codecs=pcm" : "video/webm";
		}

		// Simplified bitrate settings
		options.videoBitsPerSecond = parseInt(configureRecording.bitrate * 1024);
		if (configureRecording.bitrate < 1000) {
			options.audioBitsPerSecond = parseInt(100 * 1024);
		} else if (configureRecording.bitrate < 6000) {
			options.audioBitsPerSecond = parseInt(130 * 1024);
		} else if (configureRecording.bitrate < 20000) {
			options.audioBitsPerSecond = parseInt(256 * 1024);
		} else {
			// If configureRecording.bitrate is >= 20000, use bitsPerSecond for total bitrate
			options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024);
		}

		if (iOS && options.mimeType) {
			if (!MediaRecorder.isTypeSupported(options.mimeType)) {
				options.mimeType = "video/mp4";
				filext = ".mp4";
			}
		}

		video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options);

		//if (session.dbx){
		//	video.recorder.dropbox = await streamVideoToDropbox();  // i don't want to upload to dropbox remote streams; just local
		//}
	} else {
		options.mimeType = "audio/webm";
		if (configureRecording.usePCM) {
			if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) {
				options.mimeType = "audio/webm;codecs=pcm";
			}
		} else {
			options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024);
		}
		var stream = createMediaStream();
		video.srcObject.getAudioTracks().forEach(track => {
			stream.addTrack(track, video.srcObject);
		});

		if (iOS && options.mimeType) {
			if (!MediaRecorder.isTypeSupported(options.mimeType)) {
				options.mimeType = "video/mp4";
				filext = ".mp4";
			}
		}
		video.recorder.mediaRecorder = new MediaRecorder(stream, options);

		//if (session.dbx){
		//	video.recorder.dropbox = await streamVideoToDropbox();
		//}
	}
	
	var timestamp = Date.now();
	var filename = "";
	if (session.rpcs[UUID].label && session.rpcs[UUID].streamID) {
		filename = session.rpcs[UUID].label || session.rpcs[UUID].streamID;
	} else {
		filename = session.rpcs[UUID].label + "_" + session.rpcs[UUID].streamID;
	}
	
	filename = filename.replace(/[\W]+/g, "_");
	filename = filename.substring(0, 200);
	filename += "_" + timestamp.toString();

	var writer = writable.getWriter();
	video.recorder.writer = writer;
	readable.pipeTo(streamSaver.createWriteStream(filename.toString() + filext, video.recorder.stop));
	pokeIframeAPI("recording-started");

	log(options);

	function download() {
		const blob = new Blob(recordedBlobs, {
			type: "video/webm"
		});
		const url = window.URL.createObjectURL(blob);
		const a = document.createElement("a");
		a.style.display = "none";
		a.href = url;
		a.download = filename + filext;
		document.body.appendChild(a);
		a.click();
		setTimeout(
			function (uu, aa) {
				document.body.removeChild(aa);
				window.URL.revokeObjectURL(uu);
			},
			100,
			url,
			a
		);
	}

	function handleDataAvailable(event) {
		if (event.data && event.data.size > 0) {
			//recordedBlobs.push(event.data);
			try {
				writer.write(event.data); ////////////
				if (video.recording) {
					updateLocalRecordButton(UUID, parseInt((Date.now() - timestamp) / 1000) || 0);
				}
			} catch (e) {
				warnlog("Stream recording error or ended");
			}

			try {
				if (session.dbx && video.dropbox && video.dropbox[filename]) {
					video.dropbox[filename](event.data);
				}
				if (video.gdrive && video.gdrive[filename]) {
					video.gdrive[filename].addChunk(event.data);
				}
			} catch (e) {
				errorlog(e);
			}
		}
	}

	video.recorder.mediaRecorder.ondataavailable = handleDataAvailable;

	video.recorder.mediaRecorder.onerror = function (event) {
		errorlog(event);
		console.log("It's possible using &recordcodec=vp8 might resolve recording errors if caused by an incompatible hardware encoder or codec");
		video.recorder.stop();
		session.requestRateLimit(35, UUID);
		if (!session.cleanOutput) {
			setTimeout(function () {
				warnUser("an error occured with the media recorder; stopping recording");
			}, 1);
		}
	};

	video.recorder.mediaRecorder.onstop = function (event) {
		log("mediaRecorder stopped");
	};

	video.srcObject.onended = function (event) {
		video.recorder.stop();
		//session.requestRateLimit(35, UUID); // changed DEc 05 2023 - not sure this makes sense.
		if (!session.cleanOutput) {
			setTimeout(function () {
				warnUser("stream ended! stopping recording");
			}, 1);
		}
	};

	setTimeout(
		function (v) {
			if (v && v.recorder){
				v.recorder.mediaRecorder.start(1000);
			}
		},
		500,
		video
	); // 100ms chunks

	return;
}
function updateGdriveButton(UUID, gdrive, screen = false) {
	if (screen) {
		var elements = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + UUID + '_screen"]');
	} else {
		var elements = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + UUID + '"]');
	}
	if (elements[0]) {
		var progressPercentage = parseInt((1000 * gdrive.up) / gdrive.rec) / 10;
		elements[0].innerText = progressPercentage + "%";

		if (gdrive.state && gdrive.state == 2) {
			elements[0].innerText = "GDrive " + elements[0].innerText + " (done)";
		} else {
			elements[0].innerText += ", " + convertKilobytes(gdrive.rec) + " left";
		}
	}
}

function updateRemoteRecordButton(UUID, recorder, screen = false) {
	if (screen) {
		var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '_screen"]');
	} else {
		var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '"]');
	}
	if (elements[0]) {
		var time = parseInt(recorder) || 0;
		if (time == -4) {
			if (!session.cleanOutput) {
				warnUser("A remote recording has stopped unexpectedly.\n\nDid a user cancel the file downlaod?");
			}
			if (session.beepToNotify) {
				playtone();
			}
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
			elements[0].classList.remove("shake");
			elements[0].innerHTML = '<i class="las la-stop-circle"></i> stopping...';
			setTimeout(
				function (ele) {
					ele.classList.add("shake");
				},
				10,
				elements[0]
			);
		} else if (time == -3) {
			elements[0].classList.remove("pressed");
			elements[0].ariaPressed = "false";
			elements[0].disabled = true;
			elements[0].innerHTML = '<i class="lab la-apple"></i> Not Supported';
			if (!session.cleanOutput) {
				setTimeout(function () {
					warnUser("The remote browser does not support recording.\n\nPerhaps try local recording instead.");
				}, 0);
			}
		} else if (time == -5) {
			if (!session.cleanOutput) {
				setTimeout(function () {
					warnUser("The remote browser has only experimental support for media recording.\n\nAlso, when this download stops, the remote user may be asked to download the file for it to save.");
				}, 0);
			}
		} else if (time == -2) {
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
			elements[0].innerHTML = '<i class="las la-stop-circle"></i> stopping...';
		} else if (time == -1) {
			elements[0].classList.remove("pressed");
			elements[0].ariaPressed = "false";
			elements[0].innerHTML = '<i class="las la-circle"></i> <span data-translate="record-remote"> Record Remote</span>';
		} else {
			var minutes = Math.floor(time / 60);
			var seconds = time - minutes * 60;
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
			elements[0].innerHTML = '<i class="las la-stop-circle"></i> ' + minutes + "m : " + zpadTime(seconds) + "s";
		}
	}
}

function updateLocalRecordButton(UUID, recorder) {
	var elements = document.querySelectorAll('[data-action-type="recorder-local"][data--u-u-i-d="' + UUID + '"]');
	if (elements[0]) {
		var time = parseInt(recorder) || 0;

		//target.innerHTML = '<i class="las la-check"></i> <span data-translate="record"> ARMED</span>';
		//
		if (time == -3) {
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
			elements[0].innerHTML = '<i class="las la-check"></i> <span data-translate="record"> ARMED</span>';
			elements[0].style.backgroundColor = "#BF3F3F";
		} else if (time == -2) {
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
			elements[0].innerHTML = '<i class="las la-stop-circle"></i> stopping...';
			elements[0].style.backgroundColor = "";
		} else if (time == -1) {
			elements[0].classList.remove("pressed");
			elements[0].ariaPressed = "false";
			elements[0].innerHTML = '<i class="las la-circle"></i> <span data-translate="record-local"> Record Local</span>';
			elements[0].style.backgroundColor = "";
		} else {
			var minutes = Math.floor(time / 60);
			var seconds = time - minutes * 60;
			elements[0].classList.add("pressed");
			elements[0].ariaPressed = "true";
			elements[0].innerHTML = '<i class="las la-stop-circle"></i> ' + minutes + "m : " + zpadTime(seconds) + "s";
			elements[0].style.backgroundColor = "";
		}
	}
}

async function recordLocalVideoToggle() {
	if (!session.videoElement) {
		return;
	}
	log("recordLocalVideoToggle()");

	var ele = getById("recordLocalbutton");
	if (ele.dataset.state == "0") {
		if (session.videoElement.recorder && session.videoElement.recorder.closing) {
			warnlog("already closing");
			getById("recordLocalbutton").classList.remove("shake");
			getById("recordLocalbutton").classList.add("shake");
			setTimeout(function () {
				getById("recordLocalbutton").classList.remove("shake");
			}, 1000);

			return false;
		}

		ele.dataset.state = "1";
		ele.style.backgroundColor = "red";
		ele.innerHTML = '<i class="toggleSize las la-square" ></i>';
		if ("recording" in session.videoElement) {
			errorlog("its already recording ??");
		} else {
			var res = await recordLocalVideo("start");
			log(res);
		}

		if (session.director) {
			var elements = document.querySelectorAll('[data-action-type="recorder-local"][data-sid="' + session.streamID + '"]');
			if (elements[0]) {
				elements[0].classList.add("pressed");
				elements[0].ariaPressed = "true";
				elements[0].innerHTML = '<i class="las la-stop-circle"></i><span data-translate="record-local"> Record</span>';
			}
		}
		return true;
	} else {
		if ("recording" in session.videoElement) {
			var res = await recordLocalVideo("stop");
			log(res);
		}
		ele.dataset.state = "0";
		ele.style.backgroundColor = "";
		ele.innerHTML = '<i class="toggleSize las la-dot-circle" ></i>';

		if (session.director) {
			var elements = document.querySelectorAll('[data-action-type="recorder-local"][data-sid="' + session.streamID + '"]');
			if (elements[0]) {
				elements[0].classList.remove("pressed");
				elements[0].ariaPressed = "false";
				elements[0].innerHTML = '<i class="las la-circle"></i><span data-translate="record-local"> Record</span>';
			}
		}
		return false;
	}
}

function cleanupSensorData(data) {
	for (const key in data) {
		let nonNullFound = false;
		for (const subKey in data[key]) {
			if (data[key][subKey] === null) {
				delete data[key][subKey];
			} else if (subKey !== "t") {
				nonNullFound = true;
			}
		}
		if (!nonNullFound) {
			delete data[key];
		}
	}
}

function setupSensorData(pollrate = 30) {
	session.sensors = {};
	session.sensors.data = {};
	// session.sensorDataFilter = ["pos","lin","ori","mag","gyro","acc"];

	const startSensor = (SensorType, sensorKey) => {
		if (window[SensorType] && session.sensorDataFilter.includes(sensorKey)) {
			try {
				session.sensors.data[sensorKey] = {};
				let sensor = new window[SensorType]({ frequency: pollrate });
				sensor.addEventListener("reading", () => {
					try {
						session.sensors.data[sensorKey].x = sensor.x !== null ? parseFloat(sensor.x.toFixed(5)) : null;
						session.sensors.data[sensorKey].y = sensor.y !== null ? parseFloat(sensor.y.toFixed(5)) : null;
						session.sensors.data[sensorKey].z = sensor.z !== null ? parseFloat(sensor.z.toFixed(5)) : null;
					} catch (e) {}
					try {
						session.sensors.data[sensorKey].t = parseInt(Math.round(sensor.timeStamp || 0)) || Date.now();
					} catch (e) {
						errorlog(e);
					}
				});
				sensor.start();
				session.sensors[sensorKey] = sensor;
			} catch (e) {
				errorlog(e);
			}
		}
	};

	startSensor("Accelerometer", "acc");
	startSensor("Gyroscope", "gyro");
	startSensor("Magnetometer", "mag");
	startSensor("LinearAccelerationSensor", "lin");

	if (session.sensorDataFilter.includes("ori")) {
		try {
			window.addEventListener("deviceorientation", e => {
				if (e.alpha || e.beta || e.gamma || e.absolute) {
					session.sensors.data.ori = {
						a: e.alpha !== null ? parseFloat(e.alpha.toFixed(5)) : null,
						b: e.beta !== null ? parseFloat(e.beta.toFixed(5)) : null,
						g: e.gamma !== null ? parseFloat(e.gamma.toFixed(5)) : null,
						d: e.absolute || null,
						t: parseInt(Math.round(e.timeStamp || 0)) || Date.now()
					};
				}
			});
		} catch (e) {
			errorlog("Device Orientation Error:", e);
		}
	}

	let isFirstUpdate = true;
	if (navigator.geolocation && session.sensorDataFilter.includes("pos")) {
		try {
			navigator.geolocation.watchPosition(
				pos => {
					session.sensors.data.pos = {
						speed: pos.coords.speed !== null ? parseFloat(pos.coords.speed.toFixed(3)) : null,
						alt: pos.coords.altitude !== null ? parseFloat(pos.coords.altitude.toFixed(3)) : null,
						acc: pos.coords.accuracy !== null ? parseFloat(pos.coords.accuracy.toFixed(3)) : null,
						lat: pos.coords.latitude !== null ? parseFloat(pos.coords.latitude.toFixed(3)) : null,
						lon: pos.coords.longitude !== null ? parseFloat(pos.coords.longitude.toFixed(3)) : null,
						t: parseInt(Math.round(pos.timeStamp || 0)) || Date.now()
					};

					if (isFirstUpdate && (pos.coords.latitude || pos.coords.longitude)) {
						isFirstUpdate = false;
						console.log(session.sensors.data);
						warnUser("🌎🌍🌏 Geo-location sharing is enabled.\n\nIf being tracked is unwanted, please disable the 'Location' permissions in your browser's site settings.", 10000);
					}
				},
				error => {
					errorlog("Geolocation Error:", error);
					if (error.code === error.PERMISSION_DENIED) {
						warnUser("Geolocation permission was denied.");
					}
				},
				{
					enableHighAccuracy: true,
					timeout: 5000,
					maximumAge: 0
				}
			);
		} catch (e) {
			errorlog("Device Orientation Error:", e);
		}
	}

	setInterval(function () {
		if (session.sensors && session.sensors.data) {
			cleanupSensorData(session.sensors.data);
			session.sendMessage({ sensors: session.sensors.data });
		}
	}, parseInt(1000 / pollrate));
}

//// PCM 16 SAVING LOGIC
function PCM16(stream) {
	if (!stream || !stream.getAudioTracks().length) {
		errorlog("no audio track found");
		return null;
	}

	var PCM = stream.getAudioTracks()[0].getSettings();

	function audioBufferToWav(buffer, options = {}) {
		const numChannels = buffer.numberOfChannels;
		const sampleRate = buffer.sampleRate;
		const format = options.float32 ? 3 : 1;
		const bitDepth = format === 3 ? 32 : 16;

		let samples;
		if (numChannels === 2) {
			samples = interleave(buffer.getChannelData(0), buffer.getChannelData(1));
		} else {
			samples = buffer.getChannelData(0);
		}

		return encodeWAV(samples, format, sampleRate, numChannels, bitDepth);
	}

	function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) {
		const bytesPerSample = bitDepth / 8;
		const blockAlign = numChannels * bytesPerSample;
		const bufferLength = 44 + samples.length * bytesPerSample;
		const buffer = new ArrayBuffer(bufferLength);
		const dataView = new DataView(buffer);

		// referenced from: https://github.com/steveseguin/audiobuffer-to-wav (by Jam3 - MIT lic)
		writeString(dataView, 0, "RIFF");
		dataView.setUint32(4, 36 + samples.length * bytesPerSample, true);
		writeString(dataView, 8, "WAVE");
		writeString(dataView, 12, "fmt ");
		dataView.setUint32(16, 16, true);
		dataView.setUint16(20, format, true);
		dataView.setUint16(22, numChannels, true);
		dataView.setUint32(24, sampleRate, true);
		dataView.setUint32(28, sampleRate * blockAlign, true);
		dataView.setUint16(32, blockAlign, true);
		dataView.setUint16(34, bitDepth, true);
		writeString(dataView, 36, "data");
		dataView.setUint32(40, samples.length * bytesPerSample, true);

		if (format === 1) {
			floatTo16BitPCM(dataView, 44, samples);
		} else {
			writeFloat32(dataView, 44, samples);
		}

		return buffer;
	}
	function interleave(inputL, inputR) {
		const length = inputL.length + inputR.length;
		const result = new Float32Array(length);

		for (let index = 0, inputIndex = 0; index < length; index += 2, inputIndex++) {
			result[index] = inputL[inputIndex];
			result[index + 1] = inputR[inputIndex];
		}

		return result;
	}
	function floatTo16BitPCM(output, offset, input) {
		for (let i = 0; i < input.length; i++, offset += 2) {
			const s = Math.max(-1, Math.min(1, input[i]));
			output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
		}
	}
	function writeFloat32(output, offset, input) {
		for (let i = 0; i < input.length; i++, offset += 4) {
			output.setFloat32(offset, input[i], true);
		}
	}
	function writeString(dataView, offset, string) {
		for (let i = 0; i < string.length; i++) {
			dataView.setUint8(offset + i, string.charCodeAt(i));
		}
	}
	// end reference

	PCM.audioContext = new AudioContext({ sampleRate: PCM.sampleRate });
	PCM.source = PCM.audioContext.createMediaStreamSource(stream);
	PCM.numberOfChannels = PCM.source.channelCount;
	PCM.scriptNode = PCM.audioContext.createScriptProcessor(4096, PCM.numberOfChannels, PCM.numberOfChannels); // buffer size, input channels, output channels

	PCM.recording = false;
	PCM.audioData = [];
	for (let i = 0; i < PCM.numberOfChannels; i++) {
		PCM.audioData.push([]);
	}
	PCM.scriptNode.onaudioprocess = audioProcessingEvent => {
		if (!PCM.recording) return;
		for (let channel = 0; channel < PCM.numberOfChannels; channel++) {
			const inputData = audioProcessingEvent.inputBuffer.getChannelData(channel);
			PCM.audioData[channel].push(new Float32Array(inputData));
		}
	};
	PCM.source.connect(PCM.scriptNode);
	PCM.scriptNode.connect(PCM.audioContext.destination);

	PCM.startRecording = function () {
		PCM.audioData = [];
		for (let i = 0; i < PCM.numberOfChannels; i++) {
			PCM.audioData.push([]);
		}
		PCM.recording = true;
	};
	PCM.stopRecording = function (filename = "filename") {
		PCM.recording = false;

		const bufferLength = PCM.audioData[0].length * 4096;
		const audioBuffer = PCM.audioContext.createBuffer(PCM.numberOfChannels, bufferLength, PCM.audioContext.sampleRate);

		for (let channel = 0; channel < PCM.numberOfChannels; channel++) {
			const channelData = audioBuffer.getChannelData(channel);
			PCM.audioData[channel].forEach((chunk, index) => {
				channelData.set(chunk, index * 4096);
			});
		}
		const wavArrayBuffer = audioBufferToWav(audioBuffer);
		const blob = new Blob([wavArrayBuffer], { type: "audio/wav" });
		const url = URL.createObjectURL(blob);
		const anchor = document.createElement("a");
		anchor.href = url;
		anchor.download = filename + ".wav";
		anchor.click();
		URL.revokeObjectURL(url);
	};

	return PCM;
}
//// END OF PCM 16 SAVING CODE

async function recordLocalVideo(action = null, configureRecording = false, remote = false, altUUID = false) {
	// event.currentTarget,this.parentNode.parentNode.dataset.UUID

	if (session.record === false) {
		warnlog("recordings are disabled by decree of thy host magistrate");
	}
	log("original",configureRecording);
	if (typeof configureRecording !== "object"){
		let bitrate = configureRecording!==false ? configureRecording : (session.recordLocal !== false ? session.recordLocal : session.recordDefault);
		configureRecording = {
			bitrate: bitrate,
			usePCM: (bitrate===0 || session.pcm) ? true:false,
			audioOnly: (bitrate!==false && bitrate<=0) ? true:false
		};
	}
	
	if (remote) {
		var video = remote;
		if (remote.id === "videosource" || remote.id === "screensharesource") {
			remote = false;
		}
	} else if (altUUID) {
		var video = session.screenShareElement;
	} else {
		var video = session.videoElement;
	}

	if (!video) {
		warnlog("video not found");
		return;
	}
	
	log(video.id);

	if ("recording" in video) {
		if (action == "estop") {
			video.recorder.eStop();
			warnlog("EMERGENCY Stopping RECORDING!");
			video.recorder.stop();
			return;
		} else if (action == "stop") {
			log("Stopping RECORDING!");
			video.recorder.stop();
			return;
		} else if (action == "start") {
			errorlog("ALREADY RECORDING!");
			if (remote) {
				getById("recordLocalbutton").dataset.state = "1";
				getById("recordLocalbutton").style.backgroundColor = "red";
				getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-square" ></i>';
			}
			return;
		} else {
			errorlog("STOPPING RECORDING by default toggle!");
			video.recorder.stop();
			return;
		}
		return; // this should never happen
	} else if (action == "start") {
		if (video && video.recorder && video.recorder.closing) {
			errorlog("Ingore request. Haven't finished closing the previous recording.");
			return;
		}
		
		if (video.srcObject && video.srcObject.getTracks && !video.srcObject.getTracks().length){
			warnlog("No video or audio tracks to record");
			return;
		}

		if (!MediaRecorder) {
			var msg = {};
			msg.recorder = -3;
			if (altUUID) {
				msg.alt = true;
			}
			for (var i = 0; i < session.directorList.length; i++) {
				msg.UUID = session.directorList[i];
				session.sendMessage(msg, msg.UUID);
			}
			errorlog("no MediaRecorder");
			return;
		} else if (SafariVersion || iPad || iOS) {
			var msg = {};
			msg.recorder = -5;
			if (altUUID) {
				msg.alt = true;
			}
			for (var i = 0; i < session.directorList.length; i++) {
				msg.UUID = session.directorList[i];
				session.sendMessage(msg, msg.UUID);
			}
			log("SAFARI/IOS MODE ENABLED");
		}
		video.recording = true;
		if (remote) {
			getById("recordLocalbutton").dataset.state = "1";
			getById("recordLocalbutton").style.backgroundColor = "red";
			getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-square" ></i>';
		}
	} else if (action == "stop") {
		warnlog("stop not sensible");
		return;
	} else {
		log("action is :" + action);
		if (video && video.recorder && video.recorder.closing) {
			errorlog("Ingore request. Haven't finished closing the previous recording.");
			return;
		}

		if (!remote) {
			getById("recordLocalbutton").dataset.state = "1";
			getById("recordLocalbutton").style.backgroundColor = "red";
			getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-square" ></i>';
		}
		video.recording = true;
	}

	video.recorder = {};


	if (!configureRecording.audioOnly && configureRecording.bitrate<50){
		configureRecording.bitrate = 50;
	}

	if (typeof video.srcObject === "undefined" || !video.srcObject) {
		errorlog("video.srcObject undefined");
		return;
	}
	
	log(configureRecording);

	var timestamp = Date.now();
	var filename = "";
	if (session.label || session.streamID) {
		filename = session.label || session.streamID;
		filename = filename.replace(/[\W]+/g, "_");
		filename = filename.substring(0, 200);
	}

	filename += "_" + timestamp.toString();
	log("filename: " + filename);

	video.recorder.eStop = function () {
		try {
			video.recorder.writer.close();
			clearInterval(video.recorder.writer.interval);
		} catch (e) {}
	};

	video.recorder.stop = function (restart = false, notify = false) {
		if (session.dbx && video.dropbox && video.dropbox[filename]) {
			video.dropbox[filename](false);
		}
		if (video.gdrive && video.gdrive[filename]) {
			video.gdrive[filename].addChunk(false);
		}

		try {
			if (!remote) {
				if (restart) {
					if (getById("recordLocalbutton").dataset.state == 2) {
						getById("recordLocalbutton").dataset.state = "0";
						getById("recordLocalbutton").style.backgroundColor = "";
						getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-exclamation" ></i>';
						if (restart !== true) {
							warnUser("Media Recording Stopped due to an error: " + restart);
						} else {
							warnUser("Media Recording Stopped due to an error.");
						}
						restart = false;
					} else {
						getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-spinner" ></i>';
						getById("recordLocalbutton").dataset.state = "2";
					}
				} else {
					getById("recordLocalbutton").dataset.state = "0";
					getById("recordLocalbutton").style.backgroundColor = "";
					getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-dot-circle" ></i>';
					if (notify) {
						if (!session.cleanOutput) {
							warnUser("A recording has stopped unexpectedly.");
						}
						if (session.beepToNotify) {
							playtone();
						}
						getById("recordLocalbutton").classList.remove("shake");
						setTimeout(function () {
							getById("recordLocalbutton").classList.add("shake");
						}, 10);
					}
				}
			}
		} catch (e) {
			errorlog(e);
		}

		if (!video.recording) {
			errorlog("ALREADY STOPPED");
			return;
		}

		if (!video.recorder || video.recorder.closing) {
			errorlog("it's still closing; can't start until its done");
			return;
		}
		video.recorder.closing = true; // start the closing process

		try {
			if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) {
				if (video.recorder.mediaRecorder.state !== "inactive") {
					video.recorder.mediaRecorder.stop();
				}
			}
		} catch (e) {
			errorlog(e);
			try {
				video.recorder.mediaRecorder.stop();
			} catch (e1) {
				errorlog(e1);
			}
		}

		// video.recording = false;

		setTimeout(
			(configureRecording, altUUID, video) => {
				try {
					video.recorder.writer.close();
				} catch (e) {
					errorlog(e);
				}
				try {
					clearInterval(video.recorder.writer.interval);
				} catch (e) {
					errorlog(e);
				}
				pokeIframeAPI("recording-stopped");
				if (!remote) {
					try {
						if (session.directorUUID) {
							var msg = {};
							msg.recorder = -1;
							if (altUUID) {
								msg.alt = true;
							}
							for (var i = 0; i < session.directorList.length; i++) {
								msg.UUID = session.directorList[i];
								session.sendMessage(msg, msg.UUID);
							}
						}
					} catch (e) {
						errorlog(e);
					}
				}

				try {
					if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) {
						if (video.recorder.mediaRecorder.state !== "inactive") {
							video.recorder.mediaRecorder.stop();
						}
					}
				} catch (e) {
					errorlog(e);
					try {
						video.recorder.mediaRecorder.stop();
					} catch (e1) {
						errorlog(e1);
					}
				}
				try {
					delete video.recorder;
					delete video.recording;
				} catch (e) {}

				if (!remote) {
					if (restart) {
						setTimeout(
							function (configureRecording, altUUID) {
								recordLocalVideo("start", configureRecording, false, altUUID);
							},
							0,
							configureRecording,
							altUUID
						);
					}
				}
			},
			500,
			configureRecording,
			altUUID,
			video
		);

		if (!remote) {
			try {
				if (session.directorUUID) {
					var msg = {};
					if (notify) {
						msg.recorder = -4; // user aborted
					} else {
						msg.recorder = -2;
					}
					if (altUUID) {
						msg.alt = true;
					}
					for (var i = 0; i < session.directorList.length; i++) {
						msg.UUID = session.directorList[i];
						session.sendMessage(msg, msg.UUID);
					}
				}
			} catch (e) {
				errorlog(e);
			}
		}
	};

	let options = {};
	let filext = ".webm";
	log("setting up options");

	if (!configureRecording.audioOnly) {
		log("videoKbps: " + configureRecording.bitrate);
		var tryCodec = session.recordingVideoCodec || ""; // Simplified condition to assign tryCodec

		if (tryCodec && MediaRecorder.isTypeSupported("video/webm;codecs=" + tryCodec)) {
			if (!session.cleanOutput) {
				console.log("👍 The browser 'says' it supports " + tryCodec);
			}
			options.mimeType = "video/webm;codecs=" + tryCodec;

			if (configureRecording.usePCM) {
				// Fixed the format of the MIME type string
				var mimeTypeWithPCM = "video/x-matroska;codecs=" + tryCodec + ",pcm";
				if (MediaRecorder.isTypeSupported(mimeTypeWithPCM)) {
					options.mimeType = mimeTypeWithPCM;
				} else {
					options.mimeType = "video/webm;codecs=pcm";
				}
			}
		} else {
			// Simplified conditions for PCM support
			if (tryCodec) {
				warnlog("video/webm;codecs=" + tryCodec + " - is not supported");
			}
			options.mimeType = configureRecording.usePCM && MediaRecorder.isTypeSupported("video/webm;codecs=pcm") ? "video/webm;codecs=pcm" : "video/webm";
		}

		// Simplified bitrate settings
		options.videoBitsPerSecond = parseInt(configureRecording.bitrate * 1024);
		if (configureRecording.bitrate < 1000) {
			options.audioBitsPerSecond = parseInt(100 * 1024);
		} else if (configureRecording.bitrate < 6000) {
			options.audioBitsPerSecond = parseInt(130 * 1024);
		} else if (configureRecording.bitrate < 20000) {
			options.audioBitsPerSecond = parseInt(256 * 1024);
		} else {
			// If configureRecording.bitrate is >= 20000, use bitsPerSecond for total bitrate
			options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024);
		}

		if (iOS && options.mimeType) {
			if (!MediaRecorder.isTypeSupported(options.mimeType)) {
				options.mimeType = "video/mp4";
				filext = ".mp4";
			}
		}

		video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options);
		try {
			log(options);
			video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options);
		} catch (e) {
			warnlog(e);
			try {
				errorlog("options failed");
				video.recorder.mediaRecorder = new MediaRecorder(video.srcObject);
			} catch (e) {
				errorlog(e);
				errorlog("Failing the recording");
				var msg = {};
				msg.recorder = -3;
				if (altUUID) {
					msg.alt = true;
				}
				for (var i = 0; i < session.directorList.length; i++) {
					msg.UUID = session.directorList[i];
					session.sendMessage(msg, msg.UUID);
				}
				getById("recordLocalbutton").dataset.state = "0";
				getById("recordLocalbutton").style.backgroundColor = "";
				getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-dot-circle" ></i>';
				return;
			}
		}

		if (session.dbx) {
			if (!video.dropbox) {
				video.dropbox = {};
			}

			video.dropbox[filename] = await streamVideoToDropbox(filename.toString() + filext); // i don't want to upload to dropbox remote streams; just local
			if (!video.dropbox[filename]) {
				delete video.dropbox[filename];
			}
		}
		if (session.gdrive) {
			if (!video.gdrive) {
				video.gdrive = {};
			}
			if (session.gdrive === true) {
				video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext);
			} else if (session.gdrive.sessionUri) {
				video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext, session.gdrive.sessionUri); // filename isn't actually being used here
				session.gdrive = false;
			} else {
				errorlog("gdrive partially setup?");
				video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext);
			}
			if (!video.gdrive[filename]) {
				delete video.gdrive[filename];
			}
		}

		log(video.recorder.mediaRecorder);
	} else {
		log("Audio only?");
		options.mimeType = "audio/webm";
		if (configureRecording.usePCM) {
			if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) {
				options.mimeType = "audio/webm;codecs=pcm";
			}
		} else {
			options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024);
		}
		var stream = createMediaStream();
		var audioTrack = false;
		video.srcObject.getAudioTracks().forEach(track => {
			audioTrack = true;
			stream.addTrack(track, video.srcObject);
		});

		if (!audioTrack) {
			errorlog("Failing the recording; no audio track");
			try {
				video.recorder.writer.close();
			} catch (e) {}

			try {
				clearInterval(video.recorder.writer.interval);
			} catch (e) {}

			try {
				delete video.recorder;
				delete video.recording;
			} catch (e) {}
			var msg = {};
			msg.recorder = -3;
			if (altUUID) {
				msg.alt = true;
			}
			for (var i = 0; i < session.directorList.length; i++) {
				msg.UUID = session.directorList[i];
				session.sendMessage(msg, msg.UUID);
			}
			getById("recordLocalbutton").dataset.state = "0";
			getById("recordLocalbutton").style.backgroundColor = "";
			getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-dot-circle" ></i>';
			return;
		} else {
			if (iOS && options.mimeType) {
				if (!MediaRecorder.isTypeSupported(options.mimeType)) {
					options.mimeType = "video/mp4";
				}
			}

			try {
				video.recorder.mediaRecorder = new MediaRecorder(stream, options);
			} catch (e) {
				warnlog(e);
				try {
					errorlog("options failed. failing safe..");
					video.recorder.mediaRecorder = new MediaRecorder(stream);
				} catch (e) {
					errorlog(e);
					errorlog("Fail safe failed; closing the recording");
					try {
						video.recorder.writer.close();
					} catch (e) {}

					try {
						clearInterval(video.recorder.writer.interval);
					} catch (e) {}

					try {
						delete video.recorder;
						delete video.recording;
					} catch (e) {}
					var msg = {};
					msg.recorder = -3;
					if (altUUID) {
						msg.alt = true;
					}
					for (var i = 0; i < session.directorList.length; i++) {
						msg.UUID = session.directorList[i];
						session.sendMessage(msg, msg.UUID);
					}
					getById("recordLocalbutton").dataset.state = "0";
					getById("recordLocalbutton").style.backgroundColor = "";
					getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-dot-circle" ></i>';
					return;
				}
			}
			if (session.dbx) {
				if (!video.dropbox) {
					video.dropbox = {};
				}
				video.dropbox[filename] = await streamVideoToDropbox(filename.toString() + filext); // i don't want to upload to dropbox remote streams; just local
				if (!video.dropbox[filename]) {
					delete video.dropbox[filename];
				}
			}
			if (session.gdrive) {
				if (!video.gdrive) {
					video.gdrive = {};
				}
				if (session.gdrive === true) {
					video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext);
				} else if (session.gdrive.sessionUri) {
					video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext, session.gdrive.sessionUri); // filename isn't actually being used here
					session.gdrive = false;
				} else {
					video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext);
					errorlog("Gdrive only partially setup");
				}
				if (!video.gdrive[filename]) {
					delete video.gdrive[filename];
				}
			}
		}
	}
	log(options);
	
	function createLock() {
		let isLocked = false;
		let queue = Promise.resolve();
		
		return {
			acquire: async () => {
				const release = () => { isLocked = false; };
				while (isLocked) {
					await new Promise(resolve => setTimeout(resolve, 10));
				}
				isLocked = true;
				return release;
			}
		};
	}

	var chunkQueue = [];
	async function handleDataAvailable(event, process = true) {
		if (!video.recorder.writerLock) {
			video.recorder.writerLock = createLock();
		}

		// Handle existing queue first
		while (chunkQueue.length && process) {
			const ret = await handleDataAvailable(chunkQueue.shift(), false);
			if (ret === false) {
				return;
			}
		}

		if (event.data && event.data.size > 0) {
			try {
				const release = await video.recorder.writerLock.acquire();
				try {
					if (video && video.recorder && video.recorder.writer && video.recorder.writer._ownerWritableStream && video.recorder.writer._ownerWritableStream._state === "writable"){
						await video.recorder.writer.write(event.data);
					} else {
						throw new Error("Writer not open");
					}
				} catch (e) {
					if (process === true) {
						chunkQueue.push(event);
					} else {
						chunkQueue.unshift(event);
						release();
						return false;
					}
				} finally {
					release();
				}

				// Rest of the existing code for messaging and cloud uploads
				if (session.directorList.length) {
					if (video.recording) {
						var msg = {};
						if (altUUID) {
							msg.alt = true;
						}
						msg.recorder = parseInt((Date.now() - timestamp) / 1000) || 0;
						for (var i = 0; i < session.directorList.length; i++) {
							msg.UUID = session.directorList[i];
							session.sendMessage(msg, msg.UUID);
						}
					}
				}

				if (session.dbx && video.dropbox && video.dropbox[filename]) {
					video.dropbox[filename](event.data);
				}
				if (video.gdrive && video.gdrive[filename]) {
					video.gdrive[filename].addChunk(event.data);
				}
			} catch (e) {
				errorlog(e);
			}
		}
	}

	video.recorder.mediaRecorder.ondataavailable = (event) => {
		handleDataAvailable(event).catch(e => errorlog(e));
	};

	video.recorder.mediaRecorder.onerror = function (event) {
		errorlog(event);
		console.log("It's possible using &recordcodec=vp8 might resolve recording errors if caused by an incompatible hardware encoder or codec");
		if (event && event.error && event.error.name) {
			video.recorder.stop(event.error.name);
		} else {
			video.recorder.stop(true);
		}
	};

	video.srcObject.onended = function (event) {
		video.recorder.stop();
	};

	try {
		video.recorder.iteration = 0;

		video.recorder.setupWriter = async function setupWriter(video) {
			if (video.recorder.writer) {
				try {
					video.recorder.writer.close();
					await sleep(1000);
					// we don't cancel the interval obviously
				} catch (e) {
					errorlog(e);
				}
			}
			var { readable, writable } = new TransformStream({
				transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b)))
			});
			var writer = await writable.getWriter();
			var addon = "";
			if (video.recorder.iteration != 0) {
				addon = "_" + video.recorder.iteration;
			}
			video.recorder.iteration += 1;
			readable.pipeTo(streamSaver.createWriteStream(filename.toString() + filext + addon, video.recorder.stop));
			video.recorder.writer = writer;
		};

		await video.recorder.setupWriter(video);

		if (session.recordingInterval) {
			// minutes
			function intervalClosure(video) {
				var intervalId = setInterval(
					function (video) {
						try {
							video.recorder.setupWriter(video);
						} catch (e) {
							clearInterval(intervalId);
						}
					},
					1000 * 60 * session.recordingInterval,
					video
				);
				video.recorder.writer.interval = intervalId;
			}
			intervalClosure(video);
		}
		
		video.recorder.mediaRecorder.start(1000); // 100ms chunks
		
		log("started recording");

		pokeIframeAPI("recording-started");

		getById("recordLocalbutton").dataset.state = "1";
		getById("recordLocalbutton").style.backgroundColor = "red";
		getById("recordLocalbutton").innerHTML = '<i class="toggleSize las la-square" ></i>';

		if (session.directorList.length) {
			var msg = {};
			if (altUUID) {
				msg.alt = true;
			}
			msg.recorder = 0;
			for (var i = 0; i < session.directorList.length; i++) {
				msg.UUID = session.directorList[i];
				session.sendMessage(msg, msg.UUID);
			}
		}
	} catch (e) {
		errorlog(e);
	}
	return;
}

function localGlobalRecordStart() {
	document.querySelectorAll("[data-action-type='recorder-local']").forEach(target => {
		var UUID = target.dataset.UUID;
		if (!UUID) {
			return;
		}
		var video = session.rpcs[UUID].videoElement;
		if (!video) {
			return;
		}
		if (!video.stopWriter) {
			recordVideo(target); // if not started, start
		}
	});
	recordLocalVideo("start"); // self
}
function localGlobalRecordStop() {
	document.querySelectorAll("[data-action-type='recorder-local']").forEach(target => {
		var UUID = target.dataset.UUID;
		if (!UUID) {
			return;
		}
		var video = session.rpcs[UUID].videoElement;
		if (!video) {
			return;
		}
		if (video.stopWriter) {
			recordVideo(target); // if started, stop
		}
	});
	recordLocalVideo("stop"); // self
}
async function remoteGlobalRecordStart() {
	window.focus();
	var bitrate = await promptAlt(miscTranslations["what-bitrate"], false, false, 6000);
	document.querySelectorAll("[data-action-type='recorder-remote']").forEach(target => {
		requestVideoRecord(target, true, bitrate);
	});
}
function remoteGlobalRecordStop() {
	document.querySelectorAll("[data-action-type='recorder-remote']").forEach(target => {
		if (target.classList.contains("pressed")) {
			requestVideoRecord(target, false);
		}
	});
}
session.onTrack = function (event, UUID) {
	if (session.badStreamList.includes(session.rpcs[UUID].streamID)) {
		errorlog("new connection is contained in badStreamList 2! This shouldn't happen");
		// we will have none of this.
		return;
	}

	var newTracks = [];
	var newStream = false;
	if (event.streams && event.streams[0]) {
		newStream = event.streams[0];
		newTracks = newStream.getTracks();
	} else if (event.track) {
		newTracks.push(event.track);
	} else {
		errorlog("Something went wrong with incoming track..");
		return;
	}

	if (session.rpcs[UUID].streamSrc) {
		var tracks = session.rpcs[UUID].streamSrc.getTracks();
		for (var i = 0; i < newTracks.length; i++) {
			for (var j = 0; j < tracks.length; j++) {
				if (newTracks[i].id == tracks[j].id && newTracks[i].kind == tracks[j].kind) {
					newTracks.splice(i, 1);
					i--;
					break;
				}
			}
		}
	}

	var screenshare = false;
	if (session.rpcs[UUID].screenIndexes && session.rpcs[UUID].getReceivers && session.rpcs[UUID].screenIndexes.length) {
		log("session.rpcs[UUID].screenIndexes: " + session.rpcs[UUID].screenIndexes);
		var receievers = session.rpcs[UUID].getReceivers(); // excluded
		for (var i = 0; i < receievers.length; i++) {
			for (var j = 0; j < newTracks.length; j++) {
				if (receievers[i].track && receievers[i].track.id == newTracks[j].id && receievers[i].track.kind == newTracks[j].kind) {
					for (var k = 0; k < session.rpcs[UUID].screenIndexes.length; k++) {
						if (session.rpcs[UUID].screenIndexes[k] == i) {
							screenshare = true;
							break;
						}
					}
				}
				if (screenshare) {
					break;
				}
			}
			if (screenshare) {
				break;
			}
		}
	}

	log("screenshare: " + screenshare);
	log(session.rpcs[UUID].streamID);

	try {
		var index = newTracks.length;
		while (index--) {
			if (newTracks[index].kind == "video") {
				if (session.novideo !== false && !session.novideo.includes(session.rpcs[UUID].streamID)) {
					if (!(screenshare && session.novideo.includes(session.rpcs[UUID].streamID+":s"))){
						newTracks.splice(index, 1);
					}
					continue;
				} else if (session.rpcs[UUID].settings && session.rpcs[UUID].settings.allowscreenvideo && screenshare) {
					//newTracks.splice(index,1);
					continue;
				} else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.video) {
					newTracks.splice(index, 1);
					continue;
				}
			} else if (newTracks[index].kind == "audio") {
				if (session.noaudio !== false && !session.noaudio.includes(session.rpcs[UUID].streamID)) {
					if (!(screenshare && session.noaudio.includes(session.rpcs[UUID].streamID+":s"))){
						newTracks.splice(index, 1);
					}
					continue;
				} else if (session.excludeaudio && session.excludeaudio.includes(session.rpcs[UUID].streamID)) {
					newTracks.splice(index, 1);
					continue;
				} else if (session.rpcs[UUID].settings && session.rpcs[UUID].settings.allowscreenaudio && screenshare) {
					//newTracks.splice(index,1);
					continue;
				} else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.audio) {
					newTracks.splice(index, 1);
					continue;
				}
			}
		}
	} catch (e) {
		errorlog(e);
	}

	if (!newTracks.length) {
		warnlog("NO NEW TRACKS?");
		return;
	}

	if (session.encodedInsertableStreams && session.rpcs[UUID] && session.rpcs[UUID].getReceivers) {
		var receievers = session.rpcs[UUID].getReceivers(); // excluded
		for (var i = 0; i < receievers.length; i++) {
			for (var j = 0; j < newTracks.length; j++) {
				if (receievers[i].track && receievers[i].track.id == newTracks[j].id && receievers[i].track.kind == newTracks[j].kind) {
					try {
						setupReceiverTransform(receievers[i]);
					} catch (e) {
						errorlog(e);
					}
				}
			}
		}
	}

	if (screenshare) {
		session.setupScreenShareAddon(newTracks, UUID);
		return;
	}

	//if (session.buffer!==false){
	playoutdelay(UUID);
	//}

	session.directorSpeakerMute(); // apply any mute states to new tracks.
	session.directorDisplayMute();

	if (newStream) {
		newStream.onremovetrack = function (e1) {
			try {
				warnlog("Track was removed");
				session.rpcs[UUID].streamSrc.getTracks().forEach(trk => {
					if (trk.id == e1.track.id && trk.kind == e1.track.kind) {
						session.rpcs[UUID].streamSrc.removeTrack(trk);
					}
				});
				if (e1.track.kind == "video") {
					updateIncomingVideoElement(UUID, true, false);
				} else {
					updateIncomingVideoElement(UUID, false, true);
				}
				// updateIncomingVideoElement(UUID); // session.rpcs[UUID].videoElement.srcObject = session.rpcs[UUID].streamSrc;
				setTimeout(function () {
					updateMixer();
				}, 1);
			} catch (e) {}
		};

		newStream.onerror = function (e1) {
			errorlog(e1);
			try {
				warnlog("Track threw an error; going to reconnect it");
				session.rpcs[UUID].streamSrc.getTracks().forEach(trk => {
					try {
						if (trk.id == e1.track.id && trk.kind == e1.track.kind) {
							session.rpcs[UUID].streamSrc.removeTrack(trk);
						}
					} catch (e) {}
				});
				if (e1.track.kind == "video") {
					updateIncomingVideoElement(UUID, true, false);
				} else {
					updateIncomingVideoElement(UUID, false, true);
				}
				setTimeout(function () {
					updateMixer();
				}, 1);
			} catch (e) {
				errorlog(e);
			}
		};
	}

	createRichVideoElement(UUID);

	if (!session.rpcs[UUID].streamSrc) {
		session.rpcs[UUID].streamSrc = createMediaStream();
		mediaSourceUpdated(UUID, session.rpcs[UUID].streamID);
	}

	var videoAdded = false;
	var audioAdded = false;

	newTracks.forEach(trk => {
		if (trk.kind == "video") {
			videoAdded = true;
		} else if (trk.kind == "audio") {
			audioAdded = true;
		}
		log("adding track");
		session.rpcs[UUID].streamSrc.addTrack(trk);
	});

	if (newTracks.length > session.rpcs[UUID].streamSrc.getTracks().length) {
		errorlog("Not all the tracks were added to the local stream; are the tracks' IDs not unique?");
		console.log("streamSrc total tracks: " + session.rpcs[UUID].streamSrc.getTracks().length);
	}

	if (isIFrame && session.sendframes) {
		sendFrameHandler(newTracks, UUID);
	}

	if (audioAdded && videoAdded) {
		updateIncomingVideoElement(UUID);
	} else if (videoAdded) {
		updateIncomingVideoElement(UUID, true, false);
	} else if (audioAdded) {
		updateIncomingVideoElement(UUID, false, true);
		if (!session.roomid && session.view && !session.permaid) {
			setTimeout(function () {
				updateMixer();
			}, 10); // video already has an auto-start, with aspect ratio size change. audio doesn't.
		}
	}

	if (session.twilio && audioAdded) {
		session.twilio.updateMixer(UUID);
	}
	return session;
};

function sendFrameHandler(tracks, UUID=null){
	tracks.forEach(async trk => {
		if (trk.kind !== "video") return;
		
		log("STARTING NEW SEND STREAM VIDEO TRACK");
		
		const startImageStream = async () => {
			
			log("startImageStream");
			const canvas = document.createElement("canvas");
			const ctx = canvas.getContext("2d", { willReadFrequently: true });
			const processor = new MediaStreamTrackProcessor(trk);
			const reader = processor.readable.getReader();
			
			while (true) {
				try {
					const {done, value: frame} = await reader.read();
					if (done) break;
					try {
						canvas.width = frame.displayWidth;
						canvas.height = frame.displayHeight;
						ctx.drawImage(frame, 0, 0);
						const format = typeof session.sendframes === "string" ? session.sendframes : "webp";
						const imageData = canvas.toDataURL(`image/${format}`, 0.8);
						
						
						parent.postMessage({
							type: 'frame',
							frame: imageData,
							UUID,
							streamID: (session.rpcs[UUID] ? session.rpcs[UUID].streamID : null),
							trackID: trk.id,
							kind: trk.kind,
							format: format
						}, session.sendframes);
					} finally {
						frame.close();
					}
				} catch (e) {
					console.error("Error processing image frame:", e);
					break;
				}
			}
		};
		
		const startFrameStream = async () => {
			log("startFrameStream");
			const processor = new MediaStreamTrackProcessor(trk);
			const reader = processor.readable.getReader();
			
			while (true) {
				try {
					const {done, value} = await reader.read();
					if (done) {
						if (value) value.close();
						break;
					}
					
					try {
						parent.postMessage({
							frame: value,
							UUID,
							streamID: (session.rpcs[UUID] ? session.rpcs[UUID].streamID : null),
							trackID: trk.id,
							kind: trk.kind,
							type: "frame"
						}, session.sendframes, [value]);
					} finally {
						value.close();
					}
				} catch (e) {
					console.error("Error processing video frame:", e);
					if (e.name === "DataCloneError") {
						// Fall back to image stream if frame transfer fails
						return startImageStream();
					}
					break;
				}
			}
		};
		
		try {
			if (typeof MediaStreamTrackProcessor === 'function') {
				try {
					new SharedArrayBuffer(1);
					await startFrameStream();
				} catch(e){
					console.warn(e);
					await startImageStream();
				}
			} else {
				await startImageStream();
			}
		} catch (e) {
			console.error("Stream processing failed:", e);
		}
	});
}


function updateIncomingVideoElement(UUID, video = true, audio = true) {
	if (!session.rpcs[UUID].videoElement) {
		return;
	}
	if (!session.rpcs[UUID].streamSrc) {
		return;
	}

	if (!session.rpcs[UUID].videoElement.srcObject) {
		session.rpcs[UUID].videoElement.srcObject = createMediaStream();
	}

	if (video) {
		var tracks = session.rpcs[UUID].videoElement.srcObject.getVideoTracks(); // add video track

		session.rpcs[UUID].streamSrc.getVideoTracks().forEach(trk => {
			var added = false;
			tracks.forEach(trk2 => {
				if (trk.id == trk2.id && trk.kind == trk2.kind) {
					added = true;
				}
			});
			if (!added) {
				session.rpcs[UUID].videoElement.srcObject.getVideoTracks().forEach(trk2 => {
					// make sure only one video track is added at a time.
					session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2);
				});

				if (trk.muted && trk.kind == "video" && session.director) {
					trk.onunmute = function (e) {
						if (!session.rpcs[UUID]) {
							return;
						}
						this.onunmute = null;
						warnlog("ON UN-MUTE");
						updateIncomingVideoElement(UUID, true, false);
					};
				} else {
					if (session.rpcs[UUID].videoElement.controls) {
						session.rpcs[UUID].videoElement.controls = session.showControls || false;
						if (session.showControls === null) {
							setTimeout(
								function (ele) {
									if (ele) {
										ele.controls = true;
									}
								},
								500,
								session.rpcs[UUID].videoElement
							);
						}
					}
					session.rpcs[UUID].videoElement.srcObject.addTrack(trk);
					mediaVideoTrackUpdated(UUID, session.rpcs[UUID].streamID);
				}
			}
		});

		if ((session.motionSwitch || session.motionRecord) && !session.rpcs[UUID].motionDetectionInterval) {
			session.rpcs[UUID].motionDetectionInterval = setTimeout(function () {
				setInterval(function () {
					motionDetection(session.rpcs[UUID].videoElement, session.motionSwitch || session.motionRecord);
				}, 400);
			}, 2000);
		}
	}
	if (audio) {
		updateIncomingAudioElement(UUID); // do the same for audio now.
		if (!video) {
			if (session.rpcs[UUID] && session.rpcs[UUID].videoElement) {
				// this bit of code fixes an issue where the volume button doesn't show, after adding an audio track for the first time
				var pastMuteState = session.rpcs[UUID].videoElement.muted;
				session.rpcs[UUID].videoElement.muted = !pastMuteState;
				session.rpcs[UUID].videoElement.muted = pastMuteState;
			}
		}
	}
	if ((session.optimize===0) && session.activatedStreams.size && session.rpcs[UUID] && session.rpcs[UUID].streamID){
		if (session.activatedStreams.has(session.rpcs[UUID].streamID)){
			if (session.activatedStreamsQueue[session.rpcs[UUID].streamID]){
				let msgs = session.activatedStreamsQueue[session.rpcs[UUID].streamID];
				delete session.activatedStreamsQueue[session.rpcs[UUID].streamID];
				msgs.forEach(msgWithTime=>{
					log(msgWithTime);
					if (msgWithTime.time && (msgWithTime.time > Date.now() - 10000)){
						session.directorActions(msgWithTime.msg);
					}
				});
			}
		}
	}
}

function updateIncomingAudioElement(UUID) {
	// this can be called when turning on/off inbound audio processing.
	if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].streamSrc) {
		return;
	}

	if (!session.rpcs[UUID].videoElement.srcObject) {
		session.rpcs[UUID].videoElement.srcObject = createMediaStream();
	}

	log("updateIncomingAudioElement: " + UUID);
	if (session.audioEffects === true || session.pushLoudness || (session.rpcs[UUID].isolatedChannel !== undefined)) {
		var tracks = session.rpcs[UUID].streamSrc.getAudioTracks();
		if (tracks.length) {
			var track = tracks[0];
			track = addAudioPipeline(UUID, track);
			log(track);
			var added = false;
			var tracks2 = session.rpcs[UUID].videoElement.srcObject.getAudioTracks();
			log(tracks2);
			tracks2.forEach(trk2 => {
				if (trk2.label && trk2.label == "MediaStreamAudioDestinationNode") {
					// an old morphed node; delete it.
					session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2);
				} else if (track.id == trk2.id && track.kind == trk2.kind) {
					// maybe it didn't morph; already added either way
					added = true;
				} else if (tracks[0].id == trk2.id && tracks[0].kind == trk2.kind && track.id != tracks[0].id) {
					// remove original audio track that is now morphed
					session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2);
				}
			});
			if (!added) {
				session.rpcs[UUID].videoElement.srcObject.addTrack(track);
				mediaAudioTrackUpdated(UUID, session.rpcs[UUID].streamID);
			}
		} else {
			session.rpcs[UUID].videoElement.srcObject.getAudioTracks().forEach(trk => {
				// make sure to remove all tracks.
				session.rpcs[UUID].videoElement.srcObject.remove(trk);
			});
		}
	} else {
		var expected = [];
		tracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); // add audio tracks
		session.rpcs[UUID].streamSrc.getAudioTracks().forEach(trk => {
			var added = false;
			tracks.forEach(trk2 => {
				if (trk.id == trk2.id && trk.kind == trk2.kind) {
					added = true;
					expected.push(trk2); //
				}
			});
			if (!added) {
				session.rpcs[UUID].videoElement.srcObject.addTrack(trk);
				mediaAudioTrackUpdated(UUID, session.rpcs[UUID].streamID);
			}
		});
		tracks.forEach(trk => {
			var added = false;
			expected.forEach(trk2 => {
				if (trk.id == trk2.id && trk.kind == trk2.kind) {
					added = true;
				}
			});
			if (!added) {
				// not expected. so lets delete.
				warnlog("this shouldn't happen that often, audio track orphaned. removing it");
				session.rpcs[UUID].videoElement.srcObject.removeTrack(trk);
			}
		});
	}

	if (session.mixMinus) {
		stream = mixMinusAudio(UUID); // only works with p2p; no chunked mode.
	}
}



function cycleStyleOptions() {
	session.style += 1;
	if (session.style > 6) {
		session.style = 1;
	} else if (session.style == 4) {
		session.style = 5;
	}

	for (var UUID in session.rpcs) {
		if (session.rpcs[UUID].canvas) {
			try {
				if (session.rpcs[UUID].canvas) {
					session.rpcs[UUID].canvas.remove();
				}
			} catch (e) {}
			session.rpcs[UUID].canvas = null;
		}
		updateIncomingAudioElement(UUID);
	}
	updateMixer();
}

function addAudioPipeline(UUID, track) {
	// INBOUND AUDIO EFFECTS ; audio tracks only
	try {
		if (session.disableViewerWebAudioPipeline) {
			log("ignoring addAudioPipeline - disableViewerWebAudioPipeline is enabled (noap)");
			return track;
		}

		log("Triggered webaudio effects path");

		for (var tid in session.rpcs[UUID].inboundAudioPipeline) {
			delete session.rpcs[UUID].inboundAudioPipeline[tid]; // get rid of old nodes.
		}
		var trackid = track.id; // this is an audio track, or should be.

		session.rpcs[UUID].inboundAudioPipeline[trackid] = {};

		session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream = createMediaStream();
		session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream.addTrack(track);

		if (ChromiumVersion && session.audioEffects) {
			// I'm going to deprecate this.
			session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio = createAudioElement(); // TODO: I don't know if this mutedAudio thing matters any more, in recent versions of Chrome, since it won't play even if muted.
			session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = true;
			session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.playsinline = true; // ## Added Oct 9th 2022. Not sure it's does anything, but might help with iPhones?
			session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.srcObject = session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream; // needs to be added as an streamed element to be usable, even if its hidden
			session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = true;
			//session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.volume = 0.01;
			session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio
				.play()
				.then(_ => {
					//session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = false;
					log("playing 1");
				})
				.catch(warnlog);
		}

		// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaStreamTrackSource
		var source = session.audioCtx.createMediaStreamSource(session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream);

		//////////////////

		var screwedUp = false;
		session.rpcs[UUID].inboundAudioPipeline[trackid].destination = false;
		
		if (session.rpcs[UUID].isolatedChannel !== undefined) {
			log("Isolating channel: " + session.rpcs[UUID].isolatedChannel);
			session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
			source = isolateChannel(source, session.rpcs[UUID].isolatedChannel);
			screwedUp = true;
		}

		if (session.sync !== false) {
			log("adding a delay node to audio");
			source = addDelayNode(source, UUID, trackid);
			screwedUp = true;
		}
		if (session.style === 2) {
			log("adding a fftwave node to audio");
			try {
				if (session.rpcs[UUID].inboundAudioPipeline[trackid]) {
					// clear audioMeterGuest, if active.
					clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
				}
			} catch (e) {}
			source = fftWaveform(source, UUID, trackid);
		} else if (session.style === 3 || session.meterStyle) {
			log("adding a loudness meter node to audio");
			source = audioMeterGuest(source, UUID, trackid);
		} else if (session.audioMeterGuest) {
			log("adding a loudness meter node to audio");
			source = audioMeterGuest(source, UUID, trackid);
		} else if (session.activeSpeaker) {
			log("adding a loudness meter node to audio");
			source = audioMeterGuest(source, UUID, trackid);
		} else if (session.quietOthers) {
			log("adding a loudness meter node to audio");
			source = audioMeterGuest(source, UUID, trackid);
		} else if (session.pushLoudness) {
			source = audioMeterGuest(source, UUID, trackid);
		} else {
			try {
				if (session.rpcs[UUID].inboundAudioPipeline[trackid]) {
					// nothign active, so clear
					clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
				}
			} catch (e) {}
		}

		if (session.playChannel) {
			session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
			source = selectChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.playChannel);
			screwedUp = true;
		} else if (session.rpcs[UUID].channelOffset !== false) {
			log("custom offset set");
			session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
			source = offsetChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.rpcs[UUID].channelOffset, session.rpcs[UUID].channelWidth);
			screwedUp = true;
		} else if (session.offsetChannel !== false) {
			// proably better to do this last.
			log("adding offset channels");
			session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
			source = offsetChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.offsetChannel, session.channelWidth);
			screwedUp = true;
		} else if (session.panning !== false) {
			// proably better to do this last.
			log("adding offset channels");
			session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
			source = stereoPanning(source, UUID, trackid, session.panning);
			screwedUp = true;
		} else if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.manualSink) {
			screwedUp = true; // added June-3-22 to allow for custom outputs to different audio output destinations.
		}
		
		if (screwedUp) {
			warnlog("screwedUp mode activated. dun dun");
			if (session.rpcs[UUID].inboundAudioPipeline[trackid].destination === false) {
				session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
			}
			source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].destination);

			try {
				if (session.firstPlayTriggered && session.audioCtx.state == "suspended") {
					log("trying to resume..");
					session.audioCtx.resume();
				}
			} catch (e) {
				warnlog("session.audioCtx.resume(); failed");
			}

			return session.rpcs[UUID].inboundAudioPipeline[trackid].destination.stream.getAudioTracks()[0];
		}

		try {
			if (session.firstPlayTriggered && session.audioCtx.state == "suspended") {
				session.audioCtx.resume();
			}
		} catch (e) {
			warnlog("session.audioCtx.resume(); failed 2");
		}

		return track;
	} catch (e) {
		errorlog(e);
	}
	return track;
}

function processMiniInfoUpdate(miniInfo, UUID) {
	if ("qlr" in miniInfo) {
		session.rpcs[UUID].stats.info.quality_limitation_reason = miniInfo.qlr;
	}

	if ("con" in miniInfo) {
		session.rpcs[UUID].stats.info.conn_type = miniInfo.con;
	}

	if ("cpu" in miniInfo) {
		session.rpcs[UUID].stats.info.cpuLimited = miniInfo.cpu;
		if (session.rpcs[UUID].signalMeter) {
			if (miniInfo.cpu) {
				session.rpcs[UUID].signalMeter.dataset.cpu = "1";
			} else if ("cpu" in miniInfo) {
				session.rpcs[UUID].signalMeter.dataset.cpu = "0";
			}
		}
	}

	if ("hw_enc" in miniInfo) {
		session.rpcs[UUID].stats.info.hardware_video_encoder = miniInfo.hw_enc;
	}

	if ("bat" in miniInfo) {
		if (typeof miniInfo.bat == "number") {
			session.rpcs[UUID].stats.info.power_level = miniInfo.bat * 100;
		} else {
			session.rpcs[UUID].stats.info.power_level = null;
		}
	}
	if ("chrg" in miniInfo) {
		session.rpcs[UUID].stats.info.plugged_in = miniInfo.chrg;
	}

	if ("out" in miniInfo && "c" in miniInfo.out) {
		session.rpcs[UUID].stats.info.total_outbound_p2p_connections = miniInfo.out.c;
		if (session.showConnections && session.rpcs[UUID].connectionDetails) {
			session.rpcs[UUID].connectionDetails.innerText = "🔗" + session.rpcs[UUID].stats.info.total_outbound_p2p_connections;
			session.rpcs[UUID].connectionDetails.dataset.value = session.rpcs[UUID].stats.info.total_outbound_p2p_connections;
		}
	}

	if (session.rpcs[UUID].batteryMeter) {
		batteryMeterInfoUpdate(UUID);
	}
}

function batteryMeterInfoUpdate(UUID) {
	if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.power_level !== null) {
		var level = session.rpcs[UUID].batteryMeter.querySelector(".battery-level");
		if (level) {
			var value = session.rpcs[UUID].stats.info.power_level;
			if (value > 100) {
				value = 100;
			}
			if (value < 0) {
				value = 0;
			}
			level.style.height = parseInt(value) + "%";
			if (value < 15) {
				session.rpcs[UUID].batteryMeter.classList.remove("warn");
				session.rpcs[UUID].batteryMeter.classList.add("alert");
			} else if (value < 25) {
				session.rpcs[UUID].batteryMeter.classList.remove("alert");
				session.rpcs[UUID].batteryMeter.classList.add("warn");
			} else {
				session.rpcs[UUID].batteryMeter.classList.remove("alert");
				session.rpcs[UUID].batteryMeter.classList.remove("warn");
			}
			if (value < 100) {
				session.rpcs[UUID].batteryMeter.classList.remove("hidden");
			}
			//session.rpcs[UUID].batteryMeter.title = value+"% battery remaining";
			session.rpcs[UUID].batteryMeter.title = parseInt(value) + "% battery remaining";
		}
	}

	if (session.rpcs[UUID].stats.info && "plugged_in" in session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.plugged_in === false) {
		session.rpcs[UUID].batteryMeter.dataset.plugged = "0";
		session.rpcs[UUID].batteryMeter.classList.remove("hidden");
	} else {
		session.rpcs[UUID].batteryMeter.dataset.plugged = "1";
		// add on
		session.rpcs[UUID].batteryMeter.title = parseInt(value) + "% charging";
		session.rpcs[UUID].batteryMeter.classList.add("hidden");
	}
}

function setupGuestLabelControl(UUID) {
	var labelID = getById("label_" + UUID);
	if (labelID) {
		labelID.classList.add("contolboxLabel");
		labelID.dataset.UUID = UUID;

		if (session.rpcs[UUID].label) {
			labelID.innerText = session.rpcs[UUID].label; // Replace underscores with a Space when publishing to HTML. No Double spaces.
			labelID.classList.remove("addALabel");
		} else if (session.directorUUID === UUID) {
			miniTranslate(labelID, "main-director");
			labelID.classList.remove("addALabel");
		} else if (session.directorList.indexOf(UUID) >= 0) {
			miniTranslate(labelID, "co-director");
			labelID.classList.remove("addALabel");
		} else {
			miniTranslate(labelID, "add-a-label");
			labelID.classList.add("addALabel");
		}
		labelID.onclick = async function (ee) {
			var oldlabel = ee.target.innerText;
			if (session.rpcs[ee.target.dataset.UUID].label === false) {
				oldlabel = "";
			}
			window.focus();
			var newlabel = await promptAlt(getTranslation("new-display-name"), false, false, oldlabel);
			if (newlabel !== null) {
				newlabel = newlabel.trim();
				if (newlabel == "") {
					newlabel = false;
					if (session.directorUUID === UUID) {
						miniTranslate(ee.target, "main-director");
						//ee.target.innerHTML = getTranslation("main-director");
						ee.target.classList.remove("addALabel");
					} else if (session.directorList.indexOf(UUID) >= 0) {
						miniTranslate(ee.target, "co-director");
						//ee.target.innerHTML = getTranslation("co-director");
						ee.target.classList.remove("addALabel");
					} else {
						miniTranslate(ee.target, "add-a-label");
						//ee.target.innerHTML = getTranslation("add-a-label");
						ee.target.classList.add("addALabel");
					}
				} else {
					ee.target.innerText = newlabel;
					ee.target.classList.remove("addALabel");
				}
				var data = {};
				data.UUID = ee.target.dataset.UUID;
				data.changeLabel = true;
				data.value = newlabel;
				session.sendRequest(data, data.UUID);
				
				pokeAPI("details", getDetailedState(session.rpcs[UUID].streamID));
			}
			
		};
	}
	updateAriaLabel(UUID);
}

function updateAriaLabel(UUID = false) {
	if (!(UUID in session.rpcs)) {
		return;
	}

	var element = document.getElementById("container_" + UUID);
	if (!element) {
		return;
	}
	var sid = "";
	if (session.rpcs[UUID].streamID) {
		sid = ": " + session.rpcs[UUID].streamID;
	}

	if (session.rpcs[UUID].label) {
		element.setAttribute("aria-label", session.rpcs[UUID].label);
	} else if (session.directorUUID === UUID) {
		element.setAttribute("aria-label", (getTranslation("main-director") || "Main Director") + sid);
	} else if (session.directorList.indexOf(UUID) >= 0) {
		element.setAttribute("aria-label", (getTranslation("co-director") || "Co Director") + sid);
	} else if (sid) {
		element.setAttribute("aria-label", "ID" + sid);
	} else {
		element.setAttribute("aria-label", getTranslation("undefined") || "Undefined");
	}

	element.setAttribute("role", "region");
}

function updateLabelDirectors(UUID) {
	var elements = getById("label_" + UUID);
	if (elements) {
		if (session.rpcs[UUID].label) {
			elements.innerText = session.rpcs[UUID].label;
			elements.classList.remove("addALabel");
		} else if (session.directorUUID === UUID) {
			miniTranslate(elements, "main-director");
			elements.classList.remove("addALabel");
		} else if (session.directorList.indexOf(UUID) >= 0) {
			miniTranslate(elements, "co-director");
			elements.classList.remove("addALabel");
		} else {
			miniTranslate(elements, "add-a-label");
			elements.classList.add("addALabel");
		}
	}
	updateAriaLabel(UUID);
}

function updateLabelDirectors2(UUID) {
	var elements = getById("label_" + UUID);
	if (elements) {
		if (session.directorUUID === UUID) {
			miniTranslate(elements, "main-director");
			elements.classList.remove("addALabel");
		} else if (session.directorList.indexOf(UUID) >= 0) {
			miniTranslate(elements, "co-director");
			elements.classList.remove("addALabel");
		} else {
			miniTranslate(elements, "add-a-label");
			elements.classList.add("addALabel");
		}
	}
	updateAriaLabel(UUID);
}

function directorCoDirectorColoring(UUID) {
	if (UUID === session.directorUUID) {
		try {
			session.rpcs[UUID].stats.info.director = true;
			getById("container_" + UUID).classList.add("directorBox");
		} catch (e) {}
	} else if (session.directorList.indexOf(UUID) >= 0) {
		try {
			session.rpcs[UUID].stats.info.coDirector = true;
			addDirectorBlue(UUID);
		} catch (e) {}
	}
}

function addDirectorBlue(UUID) {
	getById("container_" + UUID).classList.add("directorBlue");
}

function soloLinkGeneratorInit(UUID) {
	document.querySelectorAll("container_" + UUID).forEach(ele => {
		ele.querySelectorAll("[data-sololink]").forEach(ele2 => {
			// value='" + soloLink + "' href='" + soloLink + "'/>" + soloLink + "</a>
			var soloLink = soloLinkGenerator(session.rpcs[UUID].streamID, false);
			ele2.value = soloLink;
			ele2.href = soloLink;
			ele2.innerText = soloLink;
		});
	});
}

function initRecordingImpossible(UUID) {
	var ele = document.querySelectorAll('[data-action-type="mute-guest"][data--u-u-i-d="' + UUID + '"]');
	if (ele) {
		ele.disabled = true;
		ele.title = getTranslation("Audio processing is disabled with this guest. Can't mute or change volume");
	}
	var ele = document.querySelectorAll('[data-action-type="volume"][data--u-u-i-d="' + UUID + '"]');
	if (ele) {
		ele.disabled = true;
		ele.title = title = getTranslation("Audio processing is disabled with this guest. Can't mute or change volume");
		ele.style.opacity = 0.2;
	}
}

function initGroupButtons(UUID) {
	var elements = document.querySelectorAll('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"]');
	for (var i = 0; i < elements.length; i++) {
		elements[i].classList.remove("pressed");
		elements[i].ariaPressed = "false";
		for (var g = 0; g < session.rpcs[UUID].group.length; g++) {
			if (elements[i].dataset.group === session.rpcs[UUID].group[g]) {
				elements[i].classList.add("pressed");
				elements[i].ariaPressed = "true";
			}
		}
	}
}

function changeGroupDirector(ele, state = null) {
	var group = ele.dataset.group;

	var index = session.group.indexOf(group);

	var change = false;

	if (state === true) {
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
		if (index === -1) {
			session.group.push(group);
			change = true;
		}
	} else if (state === false) {
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		if (index > -1) {
			session.group.splice(index, 1);
			change = true;
		}
	} else if (ele.classList.contains("pressed")) {
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		if (index > -1) {
			session.group.splice(index, 1);
			change = true;
		}
	} else {
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
		if (index === -1) {
			session.group.push(group);
			change = true;
		}
	}

	if (session.group.length || session.allowNoGroup) {
		session.sendMessage({ group: session.group.join(",") });
	} else {
		session.sendMessage({ group: false });
	}
	if (change) {
		updateMixer();
		pokeIframeAPI("group-set-updated", session.group);
	}

	if (session.group.indexOf(group) === -1) {
		return false;
	} else {
		return true;
	}
}

function changeGroupDirectorAPI(group, state = null, update = true) {
	log("changeGroupDirectorAPI()");
	group = sanitizeLabel(group);

	if (document.getElementById("container_director")) {
		var ele = getById("container_director").querySelector('[data-action-type="toggle-group"][data-group="' + group + '"]');
		if (ele) {
			if (update) {
				ele.click();
			} else if (state === true) {
				ele.classList.add("pressed");
				ele.ariaPressed = "true";
			}
			if (session.group.indexOf(group) === -1) {
				return false;
			} else {
				return true;
			}
		}
	}

	var index = session.group.indexOf(group);

	var eleGroup = getById("groups");
	eleGroup.classList.remove("hidden");
	var ele = eleGroup.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"');

	if (eleGroup.showDirector) {
		if (!ele) {
			ele = htmlToElement('<button style="margin: 0 5px 10px 5px;" data-sid="' + session.streamID + '" data-action-type="toggle-group" data-group="' + group + '"   title="Add to Group: ' + group + '" onclick="changeGroupDirector(this);"><span ><i class="las la-users" style="color:#060"></i>' + group + "</span></button>");

			var added = false;
			eleGroup.querySelectorAll("[data-group]").forEach(ele2 => {
				log(ele2);
				if (!added && ele2.dataset.group > group + "") {
					ele2.parentNode.insertBefore(ele, ele2);
					added = true;
				}
			});
			if (!added) {
				eleGroup.appendChild(ele);
			}
		}
	} else 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 = '<i class="las la-users" aria-hidden="true"></i><br />' + group;
		eleGroup.appendChild(ele);
		ele.onclick = function () {
			changeGroupDirectorAPI(this.dataset.group);
		};
	}
	var changed = false;

	if (state === true) {
		if (eleGroup.showDirector) {
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
		} else {
			ele.classList.add("green");
			ele.ariaPressed = "true";
		}
		if (index === -1) {
			session.group.push(group);
			changed = true;
		}
	} else if (state === false) {
		if (eleGroup.showDirector) {
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
		} else {
			ele.classList.remove("green");
			ele.ariaPressed = "false";
		}
		if (index > -1) {
			session.group.splice(index, 1);
			changed = true;
		}
	} else if (ele.classList.contains("green")) {
		if (eleGroup.showDirector) {
			ele.classList.remove("pressed");
			ele.ariaPressed = "false";
		} else {
			ele.classList.remove("green");
			ele.ariaPressed = "false";
		}
		if (index > -1) {
			session.group.splice(index, 1);
			changed = true;
		}
	} else {
		if (eleGroup.showDirector) {
			ele.classList.add("pressed");
			ele.ariaPressed = "true";
		} else {
			ele.classList.add("green");
			ele.ariaPressed = "true";
		}
		if (index === -1) {
			session.group.push(group);
			changed = true;
		}
	}
	if (update) {
		if (session.group.length || session.allowNoGroup) {
			session.sendMessage({ group: session.group.join(",") });
		} else {
			session.sendMessage({ group: false });
		}
	}
	if (changed) {
		updateMixer();
		pokeIframeAPI("group-set-updated", session.group);
	}

	if (state !== null) {
		return true;
	} else if (session.group.indexOf(group) === -1) {
		return false;
	} else {
		return true;
	}
}

function changeGroupViewDirectorAPI(group, state = null) {
	log("changeGroupViewDirectorAPI()");
	group = sanitizeLabel(group);

	var index = session.groupView.indexOf(group);

	var changed = false;

	if (state === true) {
		if (index === -1) {
			session.groupView.push(group);
			changed = true;
		}
	} else if (state === false) {
		if (index > -1) {
			session.groupView.splice(index, 1);
			changed = true;
		}
	} else {
		if (index > -1) {
			session.groupView.splice(index, 1);
		} else {
			session.groupView.push(group);
		}
		changed = true;
	}

	if (changed) {
		updateMixer();
		pokeIframeAPI("group-view-set-updated", session.groupView);
	}

	if (state !== null) {
		return true;
	} else if (session.groupView.indexOf(group) === -1) {
		return false;
	} else {
		return true;
	}
}

function changeGroup(ele, state = null) {
	var group = ele.dataset.group;

	var index = session.rpcs[ele.dataset.UUID].group.indexOf(group);
	
	var changed = false;

	if (state === true) {
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
		if (index === -1) {
			session.rpcs[ele.dataset.UUID].group.push(group);
			changed = true;
		}
	} else if (state === false) {
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		if (index > -1) {
			session.rpcs[ele.dataset.UUID].group.splice(index, 1);
			changed = true;
		}
	} else if (ele.classList.contains("pressed")) {
		ele.classList.remove("pressed");
		ele.ariaPressed = "false";
		if (index > -1) {
			session.rpcs[ele.dataset.UUID].group.splice(index, 1);
			changed = true;
		}
	} else {
		ele.classList.add("pressed");
		ele.ariaPressed = "true";
		if (index === -1) {
			session.rpcs[ele.dataset.UUID].group.push(group);
			changed = true;
		}
	}
	if (session.rpcs[ele.dataset.UUID].group.length) {
		session.sendRequest({ group: session.rpcs[ele.dataset.UUID].group.join(",") }, ele.dataset.UUID);
	} else {
		session.sendRequest({ group: false }, ele.dataset.UUID);
	}
	syncDirectorState(ele); 
	
	if (changed) {
		updateMixer();
	}

	if (session.rpcs[ele.dataset.UUID].group.indexOf(group) === -1) {
		return false;
	} else {
		return true;
	}
}

function changeChannelOffset(UUID, channel) {
	var ele = document.querySelectorAll('[data-action-type="add-channel"][data--u-u-i-d="' + UUID + '"]');
	for (var i = 0; i < ele.length; i++) {
		if (channel === i) {
			if (ele[i].classList.contains("pressed")) {
				ele[i].classList.remove("pressed");
				ele[i].ariaPressed = "false";
				channel = false;
			} else {
				ele[i].classList.add("pressed");
				ele[i].ariaPressed = "true";
			}
		} else {
			ele[i].classList.remove("pressed");
			ele[i].ariaPressed = "false";
		}
	}
	session.rpcs[UUID].channelOffset = channel;

	updateIncomingVideoElement(UUID, false, true);
	if (channel === false) {
		return false;
	} else {
		return true;
	}
}

function toggleMonoStereoMic(ele) {
	if (ele.checked) {
		session.audioInputChannels = 1;

		getById("micStereoMonoInput").checked = true;
		getById("micStereoMonoInput3").checked = true;
	} else if (urlParams.has("channelcount") || urlParams.has("ac") || urlParams.has("inputchannels")) {
		// not ideal having this here
		session.audioInputChannels = urlParams.get("channelcount") || urlParams.get("ac") || urlParams.get("inputchannels") || 0;
		session.audioInputChannels = parseInt(session.audioInputChannels);
		if (!session.audioInputChannels) {
			session.audioInputChannels = false;
		}
		getById("micStereoMonoInput").checked = false;
		getById("micStereoMonoInput3").checked = false;
	} else {
		session.audioInputChannels = false;
		getById("micStereoMonoInput").checked = false;
		getById("micStereoMonoInput3").checked = false;
	}
	try {
		activatedPreview = false;
		if (ele.id == "micStereoMonoInput3") {
			grabAudio("#audioSource3");
		} else {
			grabAudio("#audioSource");
		}
	} catch (e) {
		errorlog(e);
	}
}

function selectChannel(destination, source, channel) {
	session.audioCtx.destination.channelCountMode = "explicit";
	session.audioCtx.destination.channelInterpretation = "discrete";
	destination.channelCountMode = "explicit";
	destination.channelInterpretation = "discrete";

	try {
		destination.channelCount = 1;
	} catch (e) {
		errorlog("Max channels: " + destination.channelCount);
	}

	var splitter = session.audioCtx.createChannelSplitter(6);
	var merger = session.audioCtx.createChannelMerger(1); // mono

	source.connect(splitter);
	splitter.connect(merger, channel - 1, 0);

	return merger;
}

function offsetChannel(destination, source, offset, width = false) {
	session.audioCtx.destination.channelCountMode = "explicit";
	session.audioCtx.destination.channelInterpretation = "discrete";
	destination.channelCountMode = "explicit";
	destination.channelInterpretation = "discrete";

	try {
		destination.channelCount = session.audioChannels;
	} catch (e) {
		errorlog("Max channels: " + destination.channelCount);
	}

	if (width) {
		var splitter = session.audioCtx.createChannelSplitter(width);
		var merger = session.audioCtx.createChannelMerger(width + offset);
	} else {
		var splitter = session.audioCtx.createChannelSplitter(2);
		var merger = session.audioCtx.createChannelMerger(2 + offset);
	}

	source.connect(splitter);
	splitter.connect(merger, 0, offset);

	if (session.stereo && session.stereo != 3) {
		splitter.connect(merger, 1, 1 + offset);
	}
	return merger;
}

function addReverb(source, UUID, trackid, value) {
	// not yet actually working. requires a buffer; bleh!
	if (value === true) {
		value = Math.random() * (Math.random() * 2 - 1);
		errorlog(value);
	} else if (value === false) {
		return source;
	} else {
		value = parseFloat(value / 90) - 1 || 0;
		if (value < -1) {
			value = -1;
		}
		if (value > 1) {
			value = 1;
		}
	}
	//// some reverb logic goes here...
	///var reverbNode  = session.audioCtx.createStereoPanner();
	///session.rpcs[UUID].inboundAudioPipeline[trackid].reverbNode = reverbNode;
	////

	source.connect(reverbNode);
	return reverbNode;
}

function stereoPanning(source, UUID, trackid, value) {
	if (parseInt(value) === -1) {
		value = Math.random() * (Math.random() * 2 - 1);
		warnlog(value);
	} else if (value === false) {
		return source;
	} else if (value === true) {
		value = 90;
	} else {
		value = parseFloat(value / 90) - 1 || 0;
		if (value < -1) {
			value = -1;
		}
		if (value > 1) {
			value = 1;
		}
	}

	var gainNode = session.audioCtx.createGain();
	session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode = gainNode;
	gainNode.value = 1 - Math.abs(value) / 2; // the stereo panner seems to make things extra loud, so they clip. REDUCE IT.
	source.connect(gainNode);

	var panNode = session.audioCtx.createStereoPanner();
	session.rpcs[UUID].inboundAudioPipeline[trackid].panNode = panNode;
	panNode.pan.value = value;
	gainNode.connect(panNode);
	return panNode;
}

function adjustPan(UUID, value) {
	if (value === true) {
		value = Math.random() * (Math.random() * 2 - 1);
	} else if (value === false) {
		value = 0;
	} else {
		value = parseFloat(value / 90) - 1 || 0;
		if (value < -1) {
			value = -1;
		}
		if (value > 1) {
			value = 1;
		}
	}

	for (var trackid in session.rpcs[UUID].inboundAudioPipeline) {
		if ("panNode" in session.rpcs[UUID].inboundAudioPipeline[trackid]) {
			session.rpcs[UUID].inboundAudioPipeline[trackid].panNode.pan.setValueAtTime(value, session.audioCtx.currentTime);
		}
		if ("gainPanNode" in session.rpcs[UUID].inboundAudioPipeline[trackid]) {
			session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode.setValueAtTime(1 - Math.abs(value) / 2, session.audioCtx.currentTime);
		}
	}
}

function addDelayNode(source, UUID, trackid) {
	// append the delay Node to the track??? WOULD THIS WORK?

	var delay = parseFloat(session.sync) || 0;
	if (delay < 0) {
		delay = 0;
	}

	if ((session.audioBuffer!==false) && session.audioBuffer >= 0) {
		delay += parseFloat(session.audioBuffer);
	} else if (session.buffer && session.buffer > 0) {
		delay += parseFloat(session.buffer);
	}

	delay = delay / 1000;

	session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode = session.audioCtx.createDelay(delay + 5); // 5 seconds additionally added for the purpose of flexibility

	session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode.delayTime.value = delay; // delayTime takes it in seconds.
	source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode);
	log("added new delay node");
	return session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode;
}

function createStyleCanvas(UUID) {
	// append the delay Node to the track??? WOULD THIS WORK?
	if (!session.rpcs[UUID].canvas) {
		// just make sure that if using &effects or something, to null the canvas after use, else this won't trigger.
		session.rpcs[UUID].canvas = document.createElement("canvas");
		session.rpcs[UUID].canvas.dataset.UUID = UUID;
		if (session.rpcs[UUID].streamID) {
			session.rpcs[UUID].canvas.dataset.sid = session.rpcs[UUID].streamID;
		}
		session.rpcs[UUID].canvas.style.pointerEvents = "auto";
		session.rpcs[UUID].canvasCtx = session.rpcs[UUID].canvas.getContext("2d", { alpha: session.alpha });
		//
		session.rpcs[UUID].canvas.addEventListener("click", function (e) {
			// show stats of video if double clicked
			log("clicked");
			try {
				var uid = e.currentTarget.dataset.UUID;
				if (e.ctrlKey || e.metaKey) {
					e.preventDefault();
					if (session.statsMenu !== false) {
						if ("stats" in session.rpcs[uid]) {
							var [menu, innerMenu] = statsMenuCreator();
							printViewStats(innerMenu, uid);
							menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
						}
					}
					e.stopPropagation();
					return false;
				} else if ("prePausedBandwidth" in session.rpcs[uid]) {
					unPauseVideo(e.currentTarget);
				}
			} catch (e) {
				errorlog(e);
			}
		});

		if (session.statsMenu) {
			if ("stats" in session.rpcs[UUID]) {
				var [menu, innerMenu] = statsMenuCreator();
				printViewStats(innerMenu, UUID);
				menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID);
			}
		}

		if (session.aspectRatio) {
			if (session.aspectRatio == 1) {
				session.rpcs[UUID].canvas.width = "720";
				session.rpcs[UUID].canvas.height = "1280";
			} else if (session.aspectRatio == 2) {
				session.rpcs[UUID].canvas.width = "960";
				session.rpcs[UUID].canvas.height = "960";
			} else if (session.aspectRatio == 3) {
				session.rpcs[UUID].canvas.width = "1280";
				session.rpcs[UUID].canvas.height = "960";
			}
		} else {
			session.rpcs[UUID].canvas.width = "1280";
			session.rpcs[UUID].canvas.height = "720";
		}

		updateMixer();
		return true;
	} else {
		return false;
	}
}

function applyStyleEffect(UUID) {
	if (!session.rpcs[UUID].canvas || !session.rpcs[UUID].canvasCtx) {
		return;
	}

	/* session.rpcs[UUID].canvasContainer = document.createElement("div");
	session.rpcs[UUID].canvasContainer.appendChild(session.rpcs[UUID].canvas);
	session.rpcs[UUID].canvas.style = "width:100%;height:100%;display:block;";
	session.rpcs[UUID].canvasContainer.appendChild(session.rpcs[UUID].videoElement); */

	if (session.style == 3) {
		// black
		session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0, 0, 0)";
		session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height);
	} else if (session.style == 4) {
		session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0, 0, 0)";
		session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height);
	} else if (session.style == 5) {
		var r = Math.random() * 255;
		var g = Math.random() * 255;
		var b = Math.random() * 255;
		session.rpcs[UUID].canvasCtx.fillStyle = "rgb(" + r + ", " + g + ", " + b + ")";
		session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height);
	} else if (session.style == 6) {
		session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0,0,0)";
		session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height);

		var r = Math.random() * 150 + 50;
		var g = Math.random() * 150 + 50;
		var b = Math.random() * 150 + 50;
		session.rpcs[UUID].canvasCtx.fillStyle = "rgb(" + r + ", " + g + ", " + b + ")";
		session.rpcs[UUID].canvasCtx.beginPath();
		session.rpcs[UUID].canvasCtx.arc(parseInt(session.rpcs[UUID].canvas.width / 2), parseInt(session.rpcs[UUID].canvas.height / 2), parseInt(session.rpcs[UUID].canvas.height / 4), 0, 2 * Math.PI, false);
		session.rpcs[UUID].canvasCtx.fill();

		if (session.rpcs[UUID].label) {
			session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0,0,0)";
			session.rpcs[UUID].canvasCtx.textAlign = "center";
			session.rpcs[UUID].canvasCtx.font = parseInt(session.rpcs[UUID].canvas.height / 2.11) + "px Arial";
			session.rpcs[UUID].canvasCtx.fillText(session.rpcs[UUID].label[0].toUpperCase(), parseInt(session.rpcs[UUID].canvas.width / 2), parseInt((session.rpcs[UUID].canvas.height * 2) / 3));
		} else {
			var tmp = getComputedStyle(document.querySelector(":root")).getPropertyValue("--video-background-image").split('"');
			if (tmp.length === 3) {
				var img = new Image();
				img.onload = function () {
					session.rpcs[UUID].canvasCtx.fillStyle = "rgb(25,0,0)";
					session.rpcs[UUID].canvasCtx.drawImage(img, parseInt(session.rpcs[UUID].canvas.width / 2 - 110), parseInt(session.rpcs[UUID].canvas.height / 2 - 110), 220, 220);
				};
				img.src = tmp[1];
			}
		}
	}
}

function capitalizeFirstLetter(string) {
	return string.charAt(0).toUpperCase() + string.slice(1);
}

function fftWaveform(source, UUID, trackid) {
	// append the delay Node to the track??? WOULD THIS WORK?
	// https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode
	session.rpcs[UUID].inboundAudioPipeline[trackid].analyser = session.audioCtx.createAnalyser();
	session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.fftSize = 512;
	var bufferLength = session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.frequencyBinCount;
	var dataArray = new Uint8Array(bufferLength);
	session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteTimeDomainData(dataArray);
	// analyser.getByteTimeDomainData(dataArray);
	source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser);

	createStyleCanvas(UUID);
	clearInterval(session.rpcs[UUID].canvasIntervalAction);
	var canvasIntervalAction = setInterval(
		function (uuid) {
			if (session.style !== 2) {
				clearInterval(canvasIntervalAction); // this is FFT only, so okay to kill.
				return;
			}

			try {
				session.rpcs[uuid].inboundAudioPipeline[trackid].analyser.getByteTimeDomainData(dataArray);
				session.rpcs[uuid].canvasCtx.fillStyle = "rgba(0, 0, 0, 0.1)";
				session.rpcs[uuid].canvasCtx.fillRect(0, 0, session.rpcs[uuid].canvas.width, session.rpcs[uuid].canvas.height);
				session.rpcs[uuid].canvasCtx.lineWidth = 10;
				session.rpcs[uuid].canvasCtx.strokeStyle = "rgb(111, 255, 111)";

				var sliceWidth = (session.rpcs[uuid].canvas.width * 1.0) / bufferLength;

				var loudness = dataArray;
				var Squares = loudness.map(val => (val - 128.0) * (val - 128.0));
				var Sum = Squares.reduce((acum, val) => acum + val);
				var Mean = Sum / loudness.length;
				loudness = Math.sqrt(Mean) * 10;
				session.rpcs[uuid].stats.Audio_Loudness = parseInt(loudness);

				if (session.pushLoudness == true) {
					var loudnessObj = {};
					loudnessObj[session.rpcs[uuid].streamID] = session.rpcs[uuid].stats.Audio_Loudness;

					if (isIFrame) {
						parent.postMessage({ loudness: loudnessObj, action: "loudness", value: loudness, UUID: uuid }, session.iframetarget);
					}
				}

				if (loudness < 2) {
					return;
				}

				//log(bufferLength);
				session.rpcs[uuid].canvasCtx.beginPath();
				var m = session.rpcs[uuid].canvas.height / 256.0;
				session.rpcs[uuid].canvasCtx.moveTo(0, dataArray[0] * m);
				var x = 0;
				for (var i = 1; i < bufferLength; i++) {
					var y = dataArray[i] * m;
					session.rpcs[uuid].canvasCtx.lineTo(x, y);
					x += sliceWidth;
				}
				session.rpcs[uuid].canvasCtx.lineTo(session.rpcs[uuid].canvas.width, session.rpcs[uuid].canvas.height / 2);
				session.rpcs[uuid].canvasCtx.stroke();
			} catch (e) {
				warnlog(e);
				warnlog("Did the remote source disconnect?");
				clearInterval(canvasIntervalAction);
				warnlog(session.rpcs[uuid]);
			}
		},
		50,
		UUID
	);
	session.rpcs[UUID].canvasIntervalAction = canvasIntervalAction;
	return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser;
}

function audioMeterGuest(mediaStreamSource, UUID, trackid) {
	log("audioMeterGuest started");
	session.rpcs[UUID].inboundAudioPipeline[trackid].analyser = session.audioCtx.createAnalyser();
	mediaStreamSource.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser);
	session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.fftSize = 256;
	session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.smoothingTimeConstant = 0.05;

	var bufferLength = session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.frequencyBinCount;
	var dataArray = new Uint8Array(bufferLength);

	function updateLevels() {
		try {
			if (!session.rpcs[UUID]) {
				return;
			}

			session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteFrequencyData(dataArray);
			var total = 0;
			for (var i = 0; i < dataArray.length; i++) {
				total += dataArray[i];
			}
			total = parseInt(total / 150);
			session.rpcs[UUID].stats.Audio_Loudness = total;

			if (session.pushLoudness == true) {
				var loudnessObj = {};
				loudnessObj[session.rpcs[UUID].streamID] = session.rpcs[UUID].stats.Audio_Loudness;

				if (isIFrame) {
					parent.postMessage({ loudness: loudnessObj, action: "loudness", value: session.rpcs[UUID].stats.Audio_Loudness, UUID: UUID }, session.iframetarget);
				}
			}

			try {
				clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
				session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () {
					updateLevels();
				}, 100);
			} catch (e) {
				log("closing old inaudio pipeline");
			}

			if (session.style == 3 || session.meterStyle) {
				// overrides style
				if (session.rpcs[UUID].videoElement) {
					if (total > 40) {
						session.rpcs[UUID].videoElement.dataset.speaking = "2";
					} else if (total > 10) {
						session.rpcs[UUID].videoElement.dataset.speaking = "1";
					} else {
						session.rpcs[UUID].videoElement.dataset.speaking = "0";
					}

					if (session.meterStyle == 4) {
						session.rpcs[UUID].videoElement.dataset.loudness = total;
						return; // this is cause we are using the data-loudness
					}
				} else if (session.meterStyle == 4) {
					return;
				}
			} else if (session.scene !== false) { 
				// if a scene, cancel
				return;
			} else if (session.audioMeterGuest === false) {
				// don't show if we just want the volume levels
				return;
			}

			if (session.rpcs[UUID].voiceMeter) {
				session.rpcs[UUID].voiceMeter.dataset.level = total;
				if (session.meterStyle == 1) {
					var perct = Math.min(total, 100);

					session.rpcs[UUID].voiceMeter.style.height = perct + "%";
					if (total > 80) {
						var R = parseInt((255 * perct) / 100)
							.toString(16)
							.padStart(2, "0");
						var G = parseInt(255 - (255 * perct) / 100)
							.toString(16)
							.padStart(2, "0");
						session.rpcs[UUID].voiceMeter.style.backgroundColor = "#" + R + G + "00";
					} else {
						session.rpcs[UUID].voiceMeter.style.backgroundColor = "#00FF00";
					}
				} else {
					if (total > 15) {
						session.rpcs[UUID].voiceMeter.style.opacity = 100; // temporary
					} else {
						session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary
					}
				}
			} else {
				session.rpcs[UUID].voiceMeter = document.createElement("div");
				session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID;
				session.rpcs[UUID].voiceMeter.dataset.level = total;
				if (session.meterStyle == 1) {
					session.rpcs[UUID].voiceMeter.classList.add("video-meter2");
				} else {
					if (total > 15) {
						session.rpcs[UUID].voiceMeter.style.opacity = 100; // temporary
					} else {
						session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary
					}
					if (session.meterStyle == 2) {
						session.rpcs[UUID].voiceMeter.classList.add("video-meter-2");
					} else {
						session.rpcs[UUID].voiceMeter.classList.add("video-meter");
					}
				}
				updateMixer();
			}
		} catch (e) {
			warnlog(e);
			// fail as an exception; this is a control close.
			return;
		}
	}
	clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
	session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () {
		updateLevels();
	}, 100);
	return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser;
}

function effectsDynamicallyUpdate(event, ele) {
	log("effectsDynamicallyUpdate");
	let lastEffectValue = session.effect;
	session.effect = ele.options[ele.selectedIndex].value;

	getById("selectImageContent").style.display = "none";
	getById("selectImageContent3").style.display = "none";
	getById("selectImageOverlay").style.display = "none";
	getById("selectImageOverlay3").style.display = "none";
	getById("selectEffectAmount").style.display = "none";
	getById("selectEffectAmount3").style.display = "none";

	if (session.effect === "1") {
		updateRenderOutpipe();
		return;
	}

	if (session.effect === "7") {
        // Show zoom amount sliders
        getById("selectEffectAmount").style.display = "block";
        getById("selectEffectAmount3").style.display = "block";
        
        // Show both sets of position controls
        getById("zoomPositionControls").style.display = "block";
        getById("zoomPositionControls3").style.display = "block";
        
        if (session.effectValue_default) {
            session.effectValue = session.effectValue_default;
        } else {
            session.effectValue = 1;
        }
        
        // Set up zoom amount sliders
        const zoomInputs = ["selectEffectAmountInput", "selectEffectAmountInput3"];
        zoomInputs.forEach(id => {
            const input = getById(id);
            if (input) {
                input.min = 1;
                input.max = 4;
                input.step = 0.1;
                input.value = session.effectValue;
            }
        });
        
        // Initialize all position sliders
        const sliderPairs = [
            ["zoomPositionX1", "zoomPositionX"],
            ["zoomPositionY1", "zoomPositionY"]
        ];
        
        sliderPairs.forEach(pair => {
            pair.forEach(id => {
                const slider = getById(id);
                if (slider) {
                    if (id.includes('X')) {
                        slider.value = xPosition;
                    } else {
                        slider.value = yPosition;
                    }
                }
            });
        });
    } else {
        // Hide all position controls
        getById("zoomPositionControls").style.display = "none";
        getById("zoomPositionControls3").style.display = "none";
    }

	if (["8", "overlay"].includes(session.effect)) {
		// like zoom but none
		if (session.effect === "overlay") {
			loadOverlayImages();
		}
		updateRenderOutpipe();
		return;
	}

	if (session.effect == "3a") {
		session.effect = "3";
		session.effectValue = 5;
	}
	if (session.effectValue_default === false && session.effect == "3") {
		session.effectValue = 2;
	} else {
		session.effectValue = session.effectValue_default;
	}

	if (session.effect == "0" || !session.effect) {
		updateRenderOutpipe();
		return;
	} else if (session.effect === "3" || session.effect === "4") {
		if (!["3", "4", "5"].includes(lastEffectValue)) {
			attemptTFLiteJsFileLoad();
			if (!session.tfliteModule.looping) {
				updateRenderOutpipe();
			}
		}
		if (session.effect === "3" && session.effectValue_default == false) {
			getById("selectEffectAmount").style.display = "block";
			getById("selectEffectAmount3").style.display = "block";

			getById("selectEffectAmountInput").min = 0;
			getById("selectEffectAmountInput").max = 20;
			getById("selectEffectAmountInput").step = 1;
			getById("selectEffectAmountInput3").min = 0;
			getById("selectEffectAmountInput3").max = 20;
			getById("selectEffectAmountInput3").step = 1;

			getById("selectEffectAmountInput").value = session.effectValue;
			getById("selectEffectAmountInput3").value = session.effectValue;
		}
	} else if (session.effect === "5") {
		if (!["3", "4", "5"].includes(lastEffectValue)) {
			attemptTFLiteJsFileLoad();
			if (!session.tfliteModule.looping) {
				updateRenderOutpipe();
			}
		}
		loadContentEffectsImages();
	} else if ((session.effect === "14" || session.effect === "15") && session.effectValue_default == false) {
		getById("selectEffectAmount").style.display = "block";
		getById("selectEffectAmount3").style.display = "block";

		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 = parseInt(session.effectValue) || 25;
		getById("selectEffectAmountInput3").value = parseInt(session.effectValue) || 25;
		
		loadContentEffectsImages();
	} else if (session.effect === "6") {
		if (!gpgpuSupport) {
			if (!session.cleanOutput) {
				warnUser("Hardware acceleration isn't detected.<br /><br />This effect will not work", 4000, false);
				return;
			}
		} else if (gpgpuSupport == "Google SwiftShader") {
			if (!session.cleanOutput) {
				warnUser("Hardware acceleration isn't detected.<br /><br />Please enable it for this effect to work correctly.<br /><br /><i>Settings -> Advanced -> System -> Use hardware-accleration</i>", false, false);
			}
			return;
		}
		loadTensorflowJS();
		updateRenderOutpipe();
		//mainMeshMask();
	} else {
		//loadEffect(session.effect);
		updateRenderOutpipe();
	}

	if (session.permaid === false && session.roomid === false && session.view === false && session.director === false) {
		updateURL("effects");
	}
}

function loadContentEffectsImages() {
	if (!["5", "15"].includes(session.effect)) {
		return;
	} // only load for certain effects

	if (session.defaultBackgroundImages) {
		try {
			session.defaultBackgroundImages.reverse();
		} catch (e) {
			errorlog("Could not process image list");
			session.defaultBackgroundImages = false;
			session.selectedImage_contents = getById("selectImage_contents");
			return;
		}
		session.defaultBackgroundImages.forEach(imgSrc => {
			try {
				var img = document.createElement("img");
				img.onerror = function () {
					this.style.display = "none";
				}; // hide images that fail to load
				img.crossOrigin = "Anonymous";
				img.src = imgSrc;
				img.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;";
				img.onclick = function (event) {
					changeEffectsImage(event, this);
				};
				getById("selectImage_contents").prepend(img);
			} catch (e) {}
		});
		session.defaultBackgroundImages = false;
		session.selectedImage_contents = getById("selectImage_contents");
	} else if (!session.selectedImage_contents) {
		session.selectedImage_contents = getById("selectImage_contents");
	}
	if (document.getElementById("selectImageContent")) {
		document.getElementById("selectImageContent").style.display = "block";
		document.getElementById("selectImageContent").appendChild(session.selectedImage_contents);
		session.selectedImage_contents.classList.remove("hidden");
	} else if (document.getElementById("selectImageContent3")) {
		document.getElementById("selectImageContent3").style.display = "block";
		document.getElementById("selectImageContent3").appendChild(session.selectedImage_contents);
		session.selectedImage_contents.classList.remove("hidden");
	}
}

async function changeOverlayImage(ev, ele) {
	if (ele.files && ele.files[0]) {
		if (session.foregroundImg) {
			session.foregroundImg.classList.remove("selectedContentEffectsImage");
		}
		session.foregroundImg = document.createElement("img");
		session.foregroundImg.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;";
		session.foregroundImg.onclick = function (event) {
			changeEffectsImage(event, this);
		};
		ele.parentNode.parentNode.insertBefore(session.foregroundImg, ele.parentNode);
		session.foregroundImg.onload = () => {
			URL.revokeObjectURL(session.foregroundImg.src); // no longer needed, free memory
		};
		session.foregroundImg.src = URL.createObjectURL(ele.files[0]); // set src to blob url
		session.foregroundImg.classList.add("selectedContentEffectsImage");
	} else if (ele.tagName.toLowerCase() == "img") {
		if (session.foregroundImg) {
			session.foregroundImg.classList.remove("selectedContentEffectsImage");
		}
		session.foregroundImg = ele;
		session.foregroundImg.classList.add("selectedContentEffectsImage");
	}
}
function loadOverlayImages() {
	if (session.defaultForegroundImages) {
		try {
			session.defaultForegroundImages.reverse();
		} catch (e) {
			errorlog("Could not process image list");
			session.defaultForegroundImages = false;
			session.selectImageOverlay_contents = getById("selectImageOverlay_contents");
			return;
		}
		session.defaultForegroundImages.forEach(imgSrc => {
			try {
				var img = document.createElement("img");
				img.onerror = function () {
					this.style.display = "none";
				}; // hide images that fail to load
				img.crossOrigin = "Anonymous";
				img.src = imgSrc;
				img.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;";
				img.onclick = function (event) {
					changeOverlayImage(event, this);
				};
				getById("selectImageOverlay_contents").prepend(img);
			} catch (e) {}
		});
		session.defaultForegroundImages = false;
		session.selectImageOverlay_contents = getById("selectImageOverlay_contents");
	} else if (!session.selectImageOverlay_contents) {
		session.selectImageOverlay_contents = getById("selectImageOverlay_contents");
	}
	if (document.getElementById("selectImageOverlay")) {
		document.getElementById("selectImageOverlay").style.display = "block";
		document.getElementById("selectImageOverlay").appendChild(session.selectImageOverlay_contents);
		session.selectImageOverlay_contents.classList.remove("hidden");
	} else if (document.getElementById("selectImageOverlay3")) {
		document.getElementById("selectImageOverlay3").style.display = "block";
		document.getElementById("selectImageOverlay3").appendChild(session.selectImageOverlay_contents);
		session.selectImageOverlay_contents.classList.remove("hidden");
	}
}

var effectsLoaded = {};
var JEELIZFACEFILTER = null;
async function loadEffect(effect) {
	warnlog("effect:" + effect);
	var filename = effect.replace(/\W/g, "");
	if (effectsLoaded[filename]) {
		effectsLoaded[filename]();
		return;
	} else {
		effectsLoaded[filename] = function () {};
	}
	warnlog("Loading Effect: " + effect);
	var script = document.createElement("script");
	script.onload = async function () {
		log("LOADED EFFECT");
		effectsLoaded[filename] = await effectsEngine(filename);
		log("effectsEngine();");
		if (gpgpuSupport == "Google SwiftShader") {
			if (!session.cleanOutput) {
				warnUser("Hardware acceleration isn't detected.<br /><br />Please enable it for better performance.<br /><br /><i>Settings -> Advanced -> System -> Use hardware-accleration</i>", false, false);
			}
		}
		effectsLoaded[filename]();
	};
	script.src = "./filters/" + filename + ".js?" + parseInt(1000 * Math.random());
	document.head.appendChild(script);
	warnUser("Loading custom effects model...", 1000);
}

async function loadScript(url, callback = false) {
	var res = null;
	var rej = null;
	var promise = new Promise((resolve, reject) => {
		res = resolve;
		rej = reject;
	});

	var check = document.querySelector("script[src='" + url + "']");
	if (check) {
		if (callback) {
			callback();
		}
	} else {
		var script = document.createElement("script");
		script.type = "text/javascript";
		script.src = url;
		script.onload = function () {
			res();
			if (callback) {
				callback();
			}
		};
		document.head.appendChild(script);
	}
	return await promise;
}

var tokenClient = false;
function YoutubeChatInterface(remote = false) {
	// this lets us query Youtube for chat messages, but its quota limited :(
	if (!tokenClient) {
		tokenClient = true;
	} else {
		return;
	}

	var gisInited = false;
	var gapiInited = false;
	var busy = 0;

	function handleAuthClick() {
		tokenClient.callback = async resp => {
			if (resp.error) {
				errorlog(resp.error);
			}
			closeModal();
			var auths = gapi.client.getToken();
			if (auths) {
				setStorage("YoutubeAuth", JSON.stringify(auths), auths.expires_in || 3600);
			}
			listBroadcasts();
		};
		var saved = getStorage("YoutubeAuth");

		if (saved) {
			gapi.client.setToken(JSON.parse(saved));
			listBroadcasts();
		} else if (gapi.client.getToken() === null) {
			if (remote) {
				tokenClient.requestAccessToken({ prompt: "consent" });
			} else {
				warnUser("<button onclick='(function(){tokenClient.requestAccessToken({prompt: \"consent\"});})()'>Grant Access to Youtube Chat</button>", false, false);
			}
		} else {
			if (remote) {
				tokenClient.requestAccessToken({ prompt: "" });
			} else {
				warnUser("<button onclick='(function(){this.remove();tokenClient.requestAccessToken({prompt: \"\"});})()'>Grant Access to Youtube Chat</button>", false, false);
			}
		}
	}

	function maybeEnableButtons() {
		if (gapiInited && gisInited) {
			handleAuthClick();
		}
	}

	async function initializeGapiClient() {
		await gapi.client.init({
			apiKey: session.youtubeKey.split(",")[1],
			discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest"]
		});
		gapiInited = true;
		maybeEnableButtons();
	}

	function handleSignoutClick() {
		let token = gapi.client.getToken();
		if (token !== null) {
			google.accounts.oauth2.revoke(token.access_token);
			gapi.client.setToken("");
		}
	}

	async function listBroadcasts() {
		try {
			var response = await gapi.client.youtube.liveBroadcasts.list({
				broadcastStatus: "active"
			});
		} catch (err) {
			errorlog(err);
			return;
		}

		let broadcasts = response.result.items;
		if (!broadcasts || broadcasts.length == 0) {
			return;
		}
		broadcasts.forEach(broadcast => {
			setTimeout(
				function (liveChatId) {
					listMessages(liveChatId);
					busy += 1;
				},
				1000,
				broadcast.snippet.liveChatId
			);
		});
	}

	async function listMessages(liveChatId, pageToken = false) {
		try {
			if (pageToken) {
				var response = await gapi.client.youtube.liveChatMessages.list({
					liveChatId: liveChatId,
					part: ["id", "snippet", "authorDetails"],
					pageToken: pageToken
				});
			} else {
				var response = await gapi.client.youtube.liveChatMessages.list({
					liveChatId: liveChatId,
					part: ["id", "snippet", "authorDetails"]
				});
			}

			var messages = response.result.items;
			messages.forEach(msg => {
				pokeIframeAPI("YoutubeChat", msg);
			});

			var polling = response.result.pollingIntervalMillis;
			var pageToken = response.result.nextPageToken;

			if (busy > 1) {
				// popular eh?  Lets quickly check for more.
			} else if (busy > 0) {
				// a message ! hurrah
				if (polling < 2000) {
					polling = 2000;
				} // Was it just luck?
			} else if (polling < 5000) {
				polling = 5000; // let's not spam the api, cause we know there isn't anything waiting..
			}
			busy = 0; // reset
			setTimeout(
				function (liveChatId, pageToken) {
					listMessages(liveChatId, pageToken);
				},
				polling,
				liveChatId,
				pageToken
			);
		} catch (err) {
			return;
		}
	}

	function gisLoaded() {
		tokenClient = google.accounts.oauth2.initTokenClient({
			client_id: session.youtubeKey.split(",")[0],
			scope: "https://www.googleapis.com/auth/youtube",
			callback: ""
		});
		gisInited = true;
		maybeEnableButtons();
	}
	function gapiLoaded() {
		gapi.load("client", initializeGapiClient);
	}

	loadScript("https://apis.google.com/js/api.js", gapiLoaded);
	loadScript("https://accounts.google.com/gsi/client", gisLoaded);
}

function loadTensorflowJS() {
	if (session.TFJSModel != null) {
		return;
	}
	log("loadTensorflowJS()");
	session.TFJSModel = true;
	var script = document.createElement("script");
	var script2 = document.createElement("script");
	var script3 = document.createElement("script");
	var script4 = document.createElement("script");
	script.onload = function () {
		document.head.appendChild(script2);
	};
	script2.onload = function () {
		document.head.appendChild(script3);
	};
	script3.onload = function () {
		document.head.appendChild(script4);
	};
	script4.onload = function () {
		async function loadModel() {
			session.TFJSModel = await faceLandmarksDetection.load(faceLandmarksDetection.SupportedPackages.mediapipeFacemesh);
			closeModal();
			warnUser("Almost done loading model...", 3000);
		}
		loadModel();
	};
	script.src = "./thirdparty/tfjs/tf-core.js";
	script2.src = "./thirdparty/tfjs/tf-converter.js";
	script3.src = "./thirdparty/tfjs/tf-backend-webgl.js";
	script4.src = "./thirdparty/tfjs/face-landmarks-detection.js";
	warnUser("Downloading a big effects model... may take a minute", 15000);

	script.type = "text/javascript";
	script2.type = "text/javascript";
	script3.type = "text/javascript";
	script4.type = "text/javascript";
	document.head.appendChild(script);
}

var TFLITELOADING = false;
function attemptTFLiteJsFileLoad() {
	if (session.tfliteModule !== false) {
		return true;
	}
	warnUser("Loading effects model...");
	TFLITELOADING = true;
	session.tfliteModule = {};

	if (!document.getElementById("tflitesimdjs")) {
		var tmpScript = document.createElement("script");
		tmpScript.onload = loadTFLiteModel;
		tmpScript.type = "text/javascript";
		tmpScript.src = "./thirdparty/tflite/tflite-simd.js?ver=2";
		tmpScript.id = "tflitesimdjs";
		document.head.appendChild(tmpScript);
	}

	return false;
}
async function changeEffectsImage(ev, ele) {
	if (ele.files && ele.files[0]) {
		if (session.effectsImage) {
			session.effectsImage.classList.remove("selectedContentEffectsImage");
		}
		session.effectsImage = document.createElement("img");
		session.effectsImage.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;";
		session.effectsImage.onclick = function (event) {
			changeEffectsImage(event, this);
		};
		ele.parentNode.parentNode.insertBefore(session.effectsImage, ele.parentNode);
		session.effectsImage.onload = () => {
			URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory
		};
		session.effectsImage.src = URL.createObjectURL(ele.files[0]); // set src to blob url
		session.effectsImage.classList.add("selectedContentEffectsImage");
	} else if (ele.tagName.toLowerCase() == "img") {
		if (session.effectsImage) {
			session.effectsImage.classList.remove("selectedContentEffectsImage");
		}
		session.effectsImage = ele;
		session.effectsImage.classList.add("selectedContentEffectsImage");
	}
}

async function changeEffectAmount(ev, ele) {
	session.effectValue = ele.value;
	if (ele.id === "selectEffectAmountInput") {
		getById("selectEffectAmountInput3").value = ele.value;
	}
	log("session.effectValue: " + session.effectValue);
}
async function loadTFLiteModel() {
	try {
		if (session.tfliteModule && session.effectsImage) {
			var img = session.effectsImage;
			session.tfliteModule = await createTFLiteSIMDModule();
			session.effectsImage = img;
		} else {
			session.tfliteModule = {};
			session.tfliteModule = await createTFLiteSIMDModule();
		}
		if (!session.tfliteModule.simd) {
			var elements = document.querySelectorAll("[data-warnSimdNotice]");
			for (let i = 0; i < elements.length; i++) {
				elements[i].style.display = "inline-block";
			}
		}
	} catch (e) {
		warnlog("TF-LITE FAILED TO LOAD");
		closeModal();
		return;
	}
	const modelResponse = await fetch("./thirdparty/tflite/segm_full_v679.tflite");
	session.tfliteModule.model = await modelResponse.arrayBuffer();

	session.tfliteModule.HEAPU8.set(new Uint8Array(session.tfliteModule.model), session.tfliteModule._getModelBufferMemoryOffset());
	session.tfliteModule._loadModel(session.tfliteModule.model.byteLength);
	session.tfliteModule.activelyProcessing = false;
	TFLITELOADING = false;
	closeModal();
	if (LaunchTFWorkerCallback) {
		TFLiteWorker();
	}
}
function smdInfo() {
	warnUser("For improved performance, use Chrome v87 or newer with SIMD support enabled.<br />Enable SIMD here: <a href='chrome://flags/#enable-webassembly-simd' onclick='copyFunction(this,event)' target='_blank'>chrome://flags/#enable-webassembly-simd</a>", false, false);
}

async function startPublishing() {
	if (query("#publishOutURL input[type='text']").dataset.twitch == "true") {
		session.whipOutput = "https://g.webrtc.live-video.net:4443/v2/offer";
	} else {
		session.whipOutput = query("#publishOutURL input[type='text']").value || session.whipOutput || null;
	}

	if (!session.whipOutput) {
		warnUser("Please first provided an output destination", 2500);
		return;
	}

	if (!session.whipOutputToken) {
		session.whipOutputToken = query("#publishOutToken input[type='password']").value || false;
	}

	if (!session.whipOutputToken && query("#publishOutURL input[type='text']").dataset.twitch == "true") {
		warnUser("Please enter a Twitch stream token first", 2000);
		return;
	}
	getById("publishSettings").classList.add("hidden");

	if (!getById("whipoutvbrcbr").classList.contains("hidden")) {
		if (getById("whipoutvbrcbr").value === "cbr") {
			session.cbr = 1;
		} else {
			session.cbr = 0;
		}
	}
	if (!getById("whipoutdenoise").classList.contains("hidden")) {
		if (getById("whipoutdenoise").value === "1") {
			session.noiseSuppression = true;
		} else {
			session.noiseSuppression = false;
		}
	}
	if (!getById("whipoutisolation").classList.contains("hidden")) {
		if (getById("whipoutisolation").value === "1") {
			session.voiceIsolation = true;
		} else {
			session.voiceIsolation = false;
		}
	}
	
	if (!getById("whipoutautogain").classList.contains("hidden")) {
		if (getById("whipoutautogain").value === "1") {
			session.autoGainControl = true;
		} else {
			session.autoGainControl = false;
		}
	}
	if (!getById("whipoutstereo").classList.contains("hidden")) {
		if (getById("whipoutstereo").value === "1") {
			session.stereo = 1;
		} else {
			session.stereo = 0;
		}
	}
	if (!getById("whipoutbitrateGroupFlag").classList.contains("hidden")) {
		session.whipOutVideoBitrate = parseInt(getById("whipoutbitrateGroupFlag").value);
	}
	if (!getById("whipoutaudiobitrate").classList.contains("hidden")) {
		session.whipOutAudioBitrate = parseInt(getById("whipoutaudiobitrate").value);
	}

	var ret = await publishScreen();
	if (ret) {
		getById("publishSettings").classList.add("hidden");
		resizeWindow(1280, 720);
		document.title = "PUBLISHING🔴" + document.title;
	} else {
		getById("publishSettings").classList.remove("hidden");
	}
	return ret;
}

async function startRecording() {
	session.recordLocal = session.recordLocal || 6000;

	var ret = await publishScreen();
	if (ret) {
		getById("publishSettings").classList.add("hidden");
		resizeWindow(1280, 720);
		document.title = "RECORDING🔴" + document.title;
		recordLocalVideoToggle();
	} else {
		getById("publishSettings").classList.remove("hidden");
	}
}

function twitchSelect(ele) {
	if (ele.checked) {
		//query("#publishOutURL input[type='text']").value =
		query("#publishOutURL input[type='text']").disabled = true;
		query("#publishOutURL input[type='text']").classList.add("disable");
		query("#publishOutURL input[type='text']").dataset.twitch = "true";

		query("#publishOutToken input[type='password']").placeholder = "Twitch stream token here";
	} else {
		query("#publishOutURL input[type='text']").disabled = null;
		query("#publishOutURL input[type='text']").classList.remove("disable");
		delete getById("publishOutURL").disabled;
		query("#publishOutURL input[type='text']").dataset.twitch = "false";
		query("#publishOutToken input[type='password']").placeholder = "WHIP auth token here";
	}
}

function resizeWindow(width, height) {
	if (window.outerWidth) {
		window.resizeTo(width + (window.outerWidth - window.innerWidth), height + (window.outerHeight - window.innerHeight));
	} else {
		window.resizeTo(500, 500);
		window.resizeTo(width + (500 - document.body.offsetWidth), height + (500 - document.body.offsetHeight));
	}

	setInterval(function () {
		if (window.innerWidth / window.innerHeight > 17 / 9 && window.innerWidth / window.innerHeight < 15 / 9) {
			return;
		}
		if (window.outerWidth) {
			window.resizeTo(width + (window.outerWidth - window.innerWidth), height + (window.outerHeight - window.innerHeight));
		} else {
			window.resizeTo(500, 500);
			window.resizeTo(width + (500 - document.body.offsetWidth), height + (500 - document.body.offsetHeight));
		}
	}, 5000);
}

// compress SDP
/* 
function compressSDP(sdp) {
  // Extract critical values
  const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1];
  const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1];
  
  // Extract fingerprint (removing colons)
  const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/);
  const fingerprint = fingerprintMatch[1].replace(/:/g, '');
  
  // Extract ICE candidates if they exist
  const candidates = [];
  const candidateRegex = /a=candidate:([^\r\n]+)/g;
  let candidateMatch;
  while ((candidateMatch = candidateRegex.exec(sdp)) !== null) {
    const parts = candidateMatch[1].split(' ');
    
    // Skip IPv6 candidates
    if (parts[4].includes(':')) continue;
    
    // Only keep foundation, component, protocol, priority, ip, port, type and related values
    const foundation = parts[0];
    const component = parts[1];
    const protocol = parts[2].toLowerCase() === 'udp' ? 'u' : 't';
    const priority = parseInt(parts[3]);
    const ip = parts[4];
    const port = parseInt(parts[5]);
    const type = parts[7];
    
    // Encode type: host=h, srflx=s, relay=r
    let typeCode;
    switch(type) {
      case 'host': typeCode = 'h'; break;
      case 'srflx': typeCode = 's'; break;
      case 'relay': typeCode = 'r'; break;
      default: typeCode = 'x';
    }
    
    // Compact representation of candidate
    // Format: foundation,component,protocol,priority(shortened),ip,port,type
    candidates.push(
      `${foundation},${component},${protocol},${priorityToCompact(priority)},${ip},${port},${typeCode}`
    );
  }
  
  // Convert fingerprint from hex to a more compact representation
  const compactFingerprint = hexToCompact(fingerprint);
  
  // Build the compressed string
  // Format: C(version)|ufrag|pwd|fingerprint|[candidates]
  let result = `C1|${iceUfrag}|${icePwd}|${compactFingerprint}`;
  
  // Add candidates if they exist
  if (candidates.length > 0) {
    result += `|${candidates.join('/')}`;
  }
  
  return result;
}
function playCompressedSDP(sdp){
	sdp = decompressSDP(sdp)
	let msg = {};
	msg.description = {sdp, type:"offer"};
	msg.UUID = session.generateStreamID(15);
	session.processDescription2(msg);
}

function decompressSDP(compressed) {
  // Parse compressed string
  // Format: C(version)|ufrag|pwd|fingerprint|[candidates]
  const parts = compressed.split('|');
  
  // Version check
  if (parts[0] !== 'C1') {
    throw new Error('Unsupported compression version');
  }
  
  const iceUfrag = parts[1];
  const icePwd = parts[2];
  const fingerprint = compactToHex(parts[3]);
  
  // Format fingerprint with colons
  const formattedFingerprint = fingerprint.match(/.{2}/g).join(':');
  
  // Build the base SDP
  let sdp = [
    'v=0',
    'o=- 1 1 IN IP4 127.0.0.1',
    's=-',
    't=0 0',
    'a=group:BUNDLE 0',
    'a=extmap-allow-mixed',
    'a=msid-semantic: WMS',
    'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
    'c=IN IP4 0.0.0.0',
    `a=ice-ufrag:${iceUfrag}`,
    `a=ice-pwd:${icePwd}`,
    'a=ice-options:trickle',
    `a=fingerprint:sha-256 ${formattedFingerprint}`,
    'a=setup:actpass',
    'a=mid:0',
    'a=sctp-port:5000',
    'a=max-message-size:262144'
  ].join('\r\n');
  
  // Add candidates if they exist
  if (parts.length > 4 && parts[4]) {
    const candidates = parts[4].split('/');
    
    for (const candidate of candidates) {
      const candParts = candidate.split(',');
      
      // Parse the candidate parts
      const foundation = candParts[0];
      const component = candParts[1];
      const protocol = candParts[2] === 'u' ? 'UDP' : 'TCP';
      const priority = compactToPriority(candParts[3]);
      const ip = candParts[4];
      const port = candParts[5];
      
      // Decode type
      let type;
      switch(candParts[6]) {
        case 'h': type = 'host'; break;
        case 's': type = 'srflx'; break;
        case 'r': type = 'relay'; break;
        default: type = 'unknown';
      }
      
      // Build the candidate line
      sdp += `\r\na=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`;
    }
  }
  
  return sdp;
}

function priorityToCompact(priority) {
  // Simplified priority encoding - actual implementation would use a more sophisticated approach
  // For typical values, this could be a mapping to shorter codes
  return priority.toString(36);
}

function compactToPriority(compact) {
  return parseInt(compact, 36);
}

function hexToCompact(hex) {
  // Convert hex pairs to numbers, then to a more compressed alphabet
  // This uses a custom encoding that maps to URL-safe characters
  const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
  let result = '';
  
  // Process 3 bytes (6 hex chars) at a time to produce 4 output chars
  for (let i = 0; i < hex.length; i += 6) {
    const chunk = hex.substr(i, 6);
    if (chunk.length < 6) {
      // Handle the last partial chunk
      const value = parseInt(chunk, 16);
      result += chars[value % 64];
      if (chunk.length > 2) {
        result += chars[Math.floor(value / 64) % 64];
      }
    } else {
      // Process full 6-hex-char chunk
      const value = parseInt(chunk, 16);
      result += chars[value & 0x3F];
      result += chars[(value >> 6) & 0x3F];
      result += chars[(value >> 12) & 0x3F];
      result += chars[(value >> 18) & 0x3F];
    }
  }
  
  return result;
}

function compactToHex(compact) {
  const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
  let result = '';
  
  // Process 4 input chars at a time to produce 6 hex chars
  for (let i = 0; i < compact.length; i += 4) {
    let value = 0;
    
    for (let j = 0; j < 4 && i + j < compact.length; j++) {
      const char = compact[i + j];
      const charValue = chars.indexOf(char);
      if (charValue === -1) {
        throw new Error(`Invalid character in compact string: ${char}`);
      }
      value |= charValue << (j * 6);
    }
    
    // Convert to hex
    const hex = value.toString(16).padStart(6, '0');
    result += hex;
  }
  
  // Ensure result is the right length for a SHA-256 hash (64 hex chars)
  return result.padEnd(64, '0');
}

function compressSDPForQR(sdp) {
  // Extract critical values
  const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1];
  const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1];
  
  // Extract fingerprint (removing colons)
  const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/);
  const fingerprint = fingerprintMatch[1].replace(/:/g, '');
  
  // Extract ICE candidates if they exist
  const candidates = [];
  const candidateRegex = /a=candidate:([^\r\n]+)/g;
  let candidateMatch;
  while ((candidateMatch = candidateRegex.exec(sdp)) !== null) {
    const parts = candidateMatch[1].split(' ');
    
    // Skip IPv6 candidates
    if (parts[4].includes(':')) continue;
    
    // Compact candidate representation
    const foundation = parts[0];
    const component = parts[1];
    const protocol = parts[2].toLowerCase() === 'udp' ? '1' : '2';
    const priority = parseInt(parts[3]);
    const ip = compressIP(parts[4]);
    const port = parseInt(parts[5]);
    
    // Encode type: host=1, srflx=2, relay=3
    let typeCode;
    switch(parts[7]) {
      case 'host': typeCode = '1'; break;
      case 'srflx': typeCode = '2'; break;
      case 'relay': typeCode = '3'; break;
      default: typeCode = '0';
    }
    
    // Ultra-compact representation, optimized for QR
    candidates.push(`${foundation}${component}${protocol}${compressNumber(priority)}${ip}${port}${typeCode}`);
  }
  
  // Compact encoding of fingerprint, optimized for QR code alphanumeric mode
  const qrFingerprint = fingerprintToQR(fingerprint);
  
  // Use very compact delimiter (single character)
  let result = `Q${iceUfrag}~${icePwd}~${qrFingerprint}`;
  
  // Add candidates with minimal separator
  if (candidates.length > 0) {
    result += `~${candidates.join(',')}`;
  }
  
  return result;
}

function decompressSDPFromQR(compressed) {
  // Parse compressed string
  // Format: Q[iceUfrag]~[icePwd]~[fingerprint]~[candidates]
  if (!compressed.startsWith('Q')) {
    throw new Error('Unsupported QR compression format');
  }
  
  const parts = compressed.substring(1).split('~');
  const iceUfrag = parts[0];
  const icePwd = parts[1];
  const fingerprint = qrToFingerprint(parts[2]);
  
  // Format fingerprint with colons
  const formattedFingerprint = fingerprint.match(/.{2}/g).join(':');
  
  // Build the base SDP
  let sdp = [
    'v=0',
    'o=- 1 1 IN IP4 127.0.0.1',
    's=-',
    't=0 0',
    'a=group:BUNDLE 0',
    'a=extmap-allow-mixed',
    'a=msid-semantic: WMS',
    'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
    'c=IN IP4 0.0.0.0',
    `a=ice-ufrag:${iceUfrag}`,
    `a=ice-pwd:${icePwd}`,
    'a=ice-options:trickle',
    `a=fingerprint:sha-256 ${formattedFingerprint}`,
    'a=setup:actpass',
    'a=mid:0',
    'a=sctp-port:5000',
    'a=max-message-size:262144'
  ].join('\r\n');
  
  // Add candidates if they exist
  if (parts.length > 3 && parts[3]) {
    const candidates = parts[3].split(',');
    
    for (const candidate of candidates) {
      // Extract the packed candidate information
      let i = 0;
      
      // Extract foundation (variable length, numeric)
      let j = i;
      while (j < candidate.length && /^\d$/.test(candidate[j])) j++;
      const foundation = candidate.substring(i, j);
      i = j;
      
      // Extract component (1 digit)
      const component = candidate.substring(i, i + 1);
      i += 1;
      
      // Extract protocol (1 digit code)
      const protocolCode = candidate.substring(i, i + 1);
      const protocol = protocolCode === '1' ? 'UDP' : 'TCP';
      i += 1;
      
      // Extract priority (variable length, encoded)
      j = i;
      while (j < candidate.length && /^[0-9A-Z]$/.test(candidate[j])) j++;
      const priorityEncoded = candidate.substring(i, j);
      const priority = decompressNumber(priorityEncoded);
      i = j;
      
      // Extract IP (encoded)
      j = i;
      while (j < candidate.length && !/^\d$/.test(candidate[j])) j++;
      const ipEncoded = candidate.substring(i, j);
      const ip = decompressIP(ipEncoded);
      i = j;
      
      // Extract port (variable length, numeric)
      j = i;
      while (j < candidate.length && /^\d$/.test(candidate[j])) j++;
      const port = candidate.substring(i, j);
      i = j;
      
      // Extract type (1 digit code)
      const typeCode = candidate.substring(i, i + 1);
      let type;
      switch(typeCode) {
        case '1': type = 'host'; break;
        case '2': type = 'srflx'; break;
        case '3': type = 'relay'; break;
        default: type = 'unknown';
      }
      
      // Build the candidate line
      sdp += `\r\na=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`;
    }
  }
  
  return sdp;
}

function compressIP(ip) {
  // For common local IPs, use a single character code
  if (ip === '127.0.0.1') return 'L';
  if (ip === '0.0.0.0') return 'Z';
  if (ip.startsWith('192.168.')) return 'P' + ip.split('.').slice(2).join('');
  if (ip.startsWith('10.')) return 'T' + ip.split('.').slice(1).join('');
  if (ip.startsWith('172.')) return 'S' + ip.split('.').slice(1).join('');
  
  // For other IPs, convert to a base36 representation
  const parts = ip.split('.');
  let num = 0;
  for (let i = 0; i < 4; i++) {
    num = num * 256 + parseInt(parts[i]);
  }
  return num.toString(36).toUpperCase();
}

function decompressIP(compressed) {
  if (compressed === 'L') return '127.0.0.1';
  if (compressed === 'Z') return '0.0.0.0';
  if (compressed.startsWith('P')) {
    const suffix = compressed.substring(1);
    if (suffix.length === 0) return '192.168.0.0';
    if (suffix.length === 1) return `192.168.${suffix}.0`;
    return `192.168.${suffix.substring(0, 1)}.${suffix.substring(1)}`;
  }
  if (compressed.startsWith('T')) {
    const suffix = compressed.substring(1);
    if (suffix.length === 0) return '10.0.0.0';
    if (suffix.length === 1) return `10.${suffix}.0.0`;
    if (suffix.length === 2) return `10.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`;
    return `10.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`;
  }
  if (compressed.startsWith('S')) {
    const suffix = compressed.substring(1);
    if (suffix.length === 0) return '172.0.0.0';
    if (suffix.length === 1) return `172.${suffix}.0.0`;
    if (suffix.length === 2) return `172.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`;
    return `172.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`;
  }
  
  // Default case: convert from base36
  const num = parseInt(compressed, 36);
  return [
    (num >> 24) & 0xFF,
    (num >> 16) & 0xFF,
    (num >> 8) & 0xFF,
    num & 0xFF
  ].join('.');
}

function compressNumber(num) {
  // Convert to base36 for compactness, prefer uppercase for QR
  return num.toString(36).toUpperCase();
}

function decompressNumber(compressed) {
  return parseInt(compressed, 36);
}

function fingerprintToQR(hex) {
  // Convert 64-char hex to a more compact representation
  // Using base36 for better QR code efficiency (alphanumeric mode)
  let result = '';
  
  // Process 4 hex chars (16 bits) at a time to produce 3 base36 chars
  for (let i = 0; i < hex.length; i += 4) {
    const chunk = hex.substr(i, 4);
    if (chunk.length < 4) {
      // Handle the last partial chunk
      const value = parseInt(chunk, 16);
      result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3);
    } else {
      // Process full chunk
      const value = parseInt(chunk, 16);
      result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3);
    }
  }
  
  return result;
}

function qrToFingerprint(qrFp) {
  let result = '';
  
  // Process 3 base36 chars at a time to produce 4 hex chars
  for (let i = 0; i < qrFp.length; i += 3) {
    const chunk = qrFp.substr(i, 3);
    const value = parseInt(chunk, 36);
    result += value.toString(16).padStart(4, '0').substring(0, 4);
  }
  
  // Ensure result has correct length for SHA-256 (64 hex chars)
  return result.padEnd(64, '0');
}

function sdpToQRCode(sdp, element) {
  const compressed = compressSDPForQR(sdp);
  
  // Use the provided qrcodejs library
  new QRCode(element, {
    text: compressed,
    width: 128,
    height: 128,
    colorDark: "#000000",
    colorLight: "#ffffff",
    correctLevel: QRCode.CorrectLevel.M // Use medium error correction
  });
  
  return compressed; // Return compressed string for reference
}

function qrToSDP(qrData) {
  return decompressSDPFromQR(qrData);
}
 */
///

function configureWhipOutSDP(description) {
	// THIS IS FOR WHIP-OUTPUT; it has

	var configs = {};

	if (SafariVersion && SafariVersion <= 13 && (iOS || iPad)) {
		return description; // skip.  Not going to try to tinker with older iOS SDPs
	} else if (session.stereo == 3 || session.stereo == 5 || session.stereo == 6 || session.stereo == 1) {
		// stereo out
		configs = { stereo: 1 };
		log("stereo enabled");
	} else if (iOS || iPad) {
		// iOS doesn't have multichannel, so why even bother
		configs = {};
	} else if (session.stereo == 4) {
		configs = { stereo: 2 };
		log("stereo enabled");
	} else {
		configs = { stereo: 0 };
	}
	
	if (session.whipOutAudioCodec === "pcm") {
		if (session.audioInputChannels && session.audioInputChannels == 1) {
			description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, false); // mono
		} else if (session.stereo) {
			description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, true); // mono
		} else {
			description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, false);
		}
	} else {
		if (session.whipOutAudioCodec){
			description.sdp = CodecsHandler.preferAudioCodec(description.sdp, session.whipOutAudioCodec, session.predAudio, session.pfecAudio); // "red" codec
		}
	
		if (session.noFEC !== null) {
			configs.useinbandfec = session.noFEC ? 0 : 1;
		}
		if (session.maxptime !== false) {
			configs.maxptime = session.maxptime;
		}
		if (session.minptime !== false) {
			configs.minptime = session.minptime;
		}
		if (session.ptime !== false) {
			configs.ptime = session.ptime;
		}
		if (session.dtx !== false) {
			configs.dtx = session.dtx; // "usedtx", if no loud audio, stops sending audio for 400ms. default.
		}

		if (session.whipOutAudioBitrate) {
			configs.maxaveragebitrate = session.whipOutAudioBitrate * 1024;
			configs.cbr = session.cbr;
		}

		if (Object.keys(configs).length) {
			log("Processing sdp of type: " + description.type + " ...");
			log(configs);
			description.sdp = CodecsHandler.setOpusAttributes(description.sdp, configs, true);
		}
	}

	if (iOS || iPad) {
		//  solves issues with iOS rotation not being correct
		if (session.removeOrientationFlag && description.sdp.includes("a=extmap:3 urn:3gpp:video-orientation\r\n")) {
			description.sdp = description.sdp.replace("a=extmap:3 urn:3gpp:video-orientation\r\n", "");
		}
	}

	if (session.screenShareState && typeof session.whipOutScreenShareCodec === "object") {
		session.whipOutScreenShareCodec.reverse().forEach(codec => {
			description.sdp = CodecsHandler.preferCodec(description.sdp, codec, session.videoErrorCorrection);

			if (session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) {
				description.sdp = CodecsHandler.setVideoBitrates(
					description.sdp,
					{
						min: parseInt((session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) / 10) || 1,
						max: session.whipOutScreenShareBitrate || session.whipOutVideoBitrate || 1
					},
					codec
				);
			}
		});
	} else if (session.screenShareState && session.whipOutScreenShareCodec) {
		description.sdp = CodecsHandler.preferCodec(description.sdp, session.whipOutScreenShareCodec, session.videoErrorCorrection);

		if (session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) {
			description.sdp = CodecsHandler.setVideoBitrates(
				description.sdp,
				{
					min: parseInt((session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) / 10) || 1,
					max: session.whipOutScreenShareBitrate || session.whipOutVideoBitrate || 1
				},
				session.whipOutScreenShareCodec
			);
		}
	} else if (typeof session.whipOutCodec === "object") {
		session.whipOutCodec.reverse().forEach(codec => {
			description.sdp = CodecsHandler.preferCodec(description.sdp, codec, session.videoErrorCorrection);

			if (session.whipOutVideoBitrate) {
				description.sdp = CodecsHandler.setVideoBitrates(
					description.sdp,
					{
						min: parseInt(session.whipOutVideoBitrate / 10) || 1,
						max: session.whipOutVideoBitrate || 1
					},
					codec
				);
			}
		});
	} else if (session.whipOutCodec) {
		description.sdp = CodecsHandler.preferCodec(description.sdp, session.whipOutCodec, session.videoErrorCorrection);
		if (session.whipOutVideoBitrate) {
			description.sdp = CodecsHandler.setVideoBitrates(
				description.sdp,
				{
					min: parseInt(session.whipOutVideoBitrate / 10) || 1,
					max: session.whipOutVideoBitrate || 1
				},
				session.whipOutCodec
			);
		}
	} else {
		log("preferring h264 in sdp");
		description.sdp = CodecsHandler.preferCodec(description.sdp, "h264", session.videoErrorCorrection); // h264 default. openh264? well, this was breaking with Pion, so, meh. whatever h264
		if (session.whipOutVideoBitrate) {
			description.sdp = CodecsHandler.setVideoBitrates(
				description.sdp,
				{
					min: parseInt(session.whipOutVideoBitrate / 10) || 1,
					max: session.whipOutVideoBitrate || 1
				},
				"h264"
			);
		}
	}

	var bitrate = 2500;
	if (session.whipOutVideoBitrate !== false) {
		bitrate = session.whipOutVideoBitrate;
	}
	if (session.screenShareState && session.whipOutScreenShareBitrate !== false) {
		bitrate = session.whipOutScreenShareBitrate;
	}

	session.whipOut.savedBitrate = bitrate; // actual target
	session.whipOut.setBitrate = bitrate; // max

	return description;
}
const scalabilityModes = ["L1T1", "L1T2", "L1T3", "L2T1", "L2T2", "L2T3", "L3T1", "L3T2", "L3T3", "L2T1h", "L2T2h", "L2T3h", "S2T1", "S2T2", "S2T3", "S2T1h", "S2T2h", "S2T3h", "S3T1", "S3T2", "S3T3", "S3T1h", "S3T2h", "S3T3h", "L2T2_KEY", "L2T3_KEY", "L3T2_KEY", "L3T3_KEY"];

function completeLocationURL(originalURL, locationURL) {
	if (!originalURL) {
		return locationURL;
	} else if (!locationURL) {
		return originalURL;
	}
	const parsedOriginalURL = new URL(originalURL);

	// Check if the location URL is already absolute
	if (locationURL.startsWith("http://") || locationURL.startsWith("https://")) {
		return locationURL;
	} else {
		// Check if the original URL's pathname ends with a slash or not
		let basePath = parsedOriginalURL.origin + parsedOriginalURL.pathname;
		if (!basePath.endsWith("/")) {
			// If the pathname does not end with a slash, remove the last segment
			basePath = basePath.substring(0, basePath.lastIndexOf("/") + 1);
		}

		// Resolve "." and ".." in the relative URL against the base path
		const fullPath = new URL(locationURL, basePath).href;

		return fullPath;
	}
}


function getWhipOutCanvasTrack(baseRTC = session.whipOut) {
	if (!baseRTC) {
		errorlog("Meshcast/WHIP not connected; cant' create canvas for it");
	}
	if (!baseRTC.canvas) {
		baseRTC.canvas = document.createElement("canvas");
		baseRTC.canvas.width = 320;
		baseRTC.canvas.height = 180;
	}
	if (!baseRTC.ctx) {
		baseRTC.ctx = baseRTC.canvas.getContext("2d", { alpha: false });
		baseRTC.ctx.fillStyle = "#000";
		baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height);
	}

	if (!baseRTC.canvasStream) {
		(function loop() {
			baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height);
			setTimeout(loop, 250); // drawing at 30fps
		})();
		try {
			baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height);
			baseRTC.canvasStream = baseRTC.canvas.captureStream(4);
			
			baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height);
		} catch(e){
			errorlog("Error creating whip output placeholder track");
		}
	}
	var tracks = baseRTC.canvasStream.getVideoTracks();
	if (tracks.length) {
		return tracks[0]
	}
	errorlog("Meschast canvas not working");
	return false;
}


function whipOut() {
	log("whipOut");
	
	if (!session.videoElement || !session.videoElement.srcObject) {
		log("no videoElement yet created; can't do whip out until then");
		return false;
	}
	
	var candidates = [];
	var codec = false;
	var keyframe = false;
	async function whipConnect() {
		try {
			if (!session.configuration) {
				await chooseBestTURN();
			}

			if (session.encodedInsertableStreams) {
				// most servers won't support this
				session.configuration.encodedInsertableStreams = true;
			}

			if (session.bundlePolicy) {
				session.configuration.bundlePolicy = session.bundlePolicy;
			}
			var config = { ...session.configuration };
			log(config);

			// do anything whip specific here

			session.whipOut = new RTCPeerConnection(config);
			session.whipOut.stats = {};
			session.whipOut.maxBandwidth = null; // based on max available bitrate
			session.whipOut.scale = false;
			session.whipOut.offerToReceiveAudio = false;
			session.whipOut.offerToReceiveVideo = false;
			session.whipOut.keyframeTimeout = null;
		} catch (err) {
			errorlog(err);
			if (!session.cleanOutput) {
				warnUser("An RTC error occured");
			}
		}

		try {
			/// audio tracks

			var tracks = false;
			if (session.videoElement && session.videoElement.srcObject) {
				tracks = session.videoElement.srcObject.getAudioTracks();
			}
			var streamsource = false;
			
			if (!tracks || !tracks.length) {
				if (!session.audioCtx) {
					session.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
				}
				
				// Create an oscillator at an audible frequency
				const oscillator = session.audioCtx.createOscillator();
				oscillator.frequency.value = 440; // Standard A note
				
				// Create a gain to make it nearly silent
				const gainNode = session.audioCtx.createGain();
				gainNode.gain.value = 0.00001;
				
				const destination = session.audioCtx.createMediaStreamDestination();
				
				oscillator.connect(gainNode);
				gainNode.connect(destination);
				
				oscillator.start();
				
				streamsource = destination.stream;
				destination.stream.getAudioTracks().forEach(trk => {
					tracks = trk;
					
					trk.enabled = true; // Force the track to appear active
					
					const constraints = { // keep it quite and not denoised out back to zero
						channelCount: { ideal: 1 },
						autoGainControl: { ideal: false },
						echoCancellation: { ideal: false },
						noiseSuppression: { ideal: false }
					};
					
					try {
						trk.applyConstraints(constraints);
					} catch(e) {
						console.warn("Could not apply audio constraints", e);
					}
				});
			} else {
				tracks = tracks[0];
				streamsource = session.videoElement.srcObject;
			}

			if (session.audioContentHint && tracks.kind === "audio") {
				try {
					tracks.contentHint = session.audioContentHint;
				} catch (e) {
					errorlog(e);
				}
			}

			if (tracks) {
				try {
					session.whipOut.addTransceiver(tracks, {
						streams: [streamsource],
						direction: "sendonly"
					});
				} catch (e) {
					errorlog(e);
					session.whipOut.addTrack(tracks);
				}
			}
		} catch (e) {
			errorlog(e);
		}

		try {
			//// video tracks ///

			var tracks = false;
			if (session.videoElement && session.videoElement.srcObject) {
				tracks = session.videoElement.srcObject.getVideoTracks();
			}
			
			if (!tracks || !tracks.length){
				var track = getWhipOutCanvasTrack(session.whipOut);
			} else {
			 	var track = tracks[0];
			}
			
			if (track) { // it's actually just "track" now.
				if (session.screenShareState && session.screenshareContentHint && track.kind === "video") {
					try {
						track.contentHint = session.screenshareContentHint;
					} catch (e) {
						errorlog(e);
					}
				} else if (session.contentHint && track.kind === "video") {
					try {
						track.contentHint = session.contentHint;
					} catch (e) {
						errorlog(e);
					}
				}

				try {
					var transceiverSetup = {
						streams: [session.videoElement.srcObject],
						direction: "sendonly"
					};
					if (session.scalabilityMode) {
						// might be a good time to validate the scalabilityMode at this point; check to see if requested codec is available,etc.
						try {
							transceiverSetup.sendEncodings = [{ scalabilityMode: session.scalabilityMode }];
						} catch (e) {
							errorlog("Invalid scalability mode provided");
						}

						if (session.whipOutCodec && session.whipOutCodec.length) {
							//var svcCodecPref = [];
							//for (var i = 0;i<session.whipOutCodec.length;i++){
							//	svcCodecPref.push({mimeType:"video/"+session.whipOutCodec[i].toUpperCase(), clockRate: 90000}); // is it ever not 90000?
							//}
							var transceiver = false;
							try {
								transceiver = session.whipOut.addTransceiver(track, transceiverSetup);
								//transceiver.setCodecPreferences(svcCodecPref);
							} catch (e) {
								errorlog(e);

								if (!transceiver) {
									// transciever was made.
									warnUser("Could not configure WHIP output with SVC settings.", 5000);
									var transceiverSetup = {
										streams: [session.videoElement.srcObject],
										direction: "sendonly"
									};
									session.whipOut.addTransceiver(track, transceiverSetup);
								} else {
									warnUser("Could not configure WHIP output with specific codec settings.", 5000);
								}
							}
						} else {
							session.whipOut.addTransceiver(track, transceiverSetup);
						}
					} else {
						session.whipOut.addTransceiver(track, transceiverSetup);
					}
				} catch (e) {
					errorlog(e);
					try {
						session.whipOut.addTrack(track);
					} catch (e) {
						errorlog(e);
					}
				}
			

				if (session.encodedInsertableStreams) {
					// not supported by most servers
					const senders = session.whipOut.getSenders();
					for (const sender of senders) {
						try {
							setupSenderTransform(sender);
						} catch (e) {
							errorlog(e);
						}
					}
				}

				session.whipOut.onnegotiationneeded = publish; // bug: https://groups.google.com/forum/#!topic/discuss-webrtc/3-TmyjQ2SeE

				session.whipOut.onicecandidate = function (event) {
					//event
					if (event.candidate == null) {
						log("END OF ICE CANDIDATES");
						return;
					}
					//log(event.candidate);
					try {
						if (session.localNetworkOnly) {
							if (!filterIceLAN(event.candidate)) {
								return;
							}
						}
						if (session.stunOnly) { // or whatever flag you want to use
							if (!filterStunOnly(event.candidate)) {
								return;
							}
						}
					} catch (e) {
						errorlog(e);
					}

					candidates.push(event.candidate);
				};
			
			} else {
				console.log(tracks, track);
				errorlog("No audio / video track found; can't publish to WHIP");
			}
		} catch (e) {
			errorlog(e);
		}
	}
	var publishing = false;

	function publish(event) {
		if (publishing) {
			log(event);
			errorlog("onnegotiationneeded again?");
			return;
		}
		publishing = true;
		warnlog("ON NEGO NEEDED");
		warnlog(event);
		try {
			session.whipOut
				.createOffer()
				.then(function (description) {
					try {
						description = configureWhipOutSDP(description);
					} catch (e) {
						errorlog(e);
					}
					return session.whipOut.setLocalDescription(description);
				})
				.then(async function () {
					try {
						if (session.whipWait) {
							console.log("Waiting for ice candidates to collect. At least 300ms recommended; at most 30-seconds.");
							let startTime = Date.now();
							const { promise, resolve } = sleepCancellable(session.whipWait); // 2000ms default; I want to give the ICE / STUN / TURN time to collect.
							session.whipOut.iceCompletedCallback = resolve; // Can complete earlier if possible.
							await promise; // pausing for a moment; until all collected or timed out
							console.log("Finished waiting for ice candidates. Waited " + (Date.now() - startTime) / 1000 + "-seconds");
							delete session.whipOut.iceCompletedCallback;
						}
					} catch (e) {
						errorlog(e);
					}
					
					//warnlog(session.whipOut.localDescription.sdp);
					var sdp = session.whipOut.localDescription.sdp;
					
					// sdp = configureWhipOutSDP(sdp);
					
					if (sdp.includes("sendrecv")) {
						errorlog("Should not include sendrecv");
						sdp = sdp.replace("a=sendrecv", "a=sendonly");
						sdp = sdp.replace("v=sendrecv", "v=sendonly");
					}
					
					sdp = cleanupStereoSettings(sdp);
					
					ajax(sdp, "sdp");
				})
				.catch(function (err) {});
		} catch (e) {
			errorlog(e);
		}
	}
	
	function cleanupStereoSettings(sdp) {
		let lines = sdp.split('\n');
		lines = lines.map(line => {
			if (line.startsWith('a=fmtp:') && (line.includes('sprop-stereo=0;') || line.includes('stereo=0;'))) {
				line = line.replace('sprop-stereo=0;', '').replace('stereo=0;', '');
				line = line.replace(';;', ';').replace(/;$/, '');
			}
			return line;
		});
		return lines.join('\n');
	}

	function isSDP(str) {
		if (typeof str !== "string" || str.trim() === "") {
			return false;
		}

		if (!str.startsWith("v=")) {
			return false;
		}

		const lines = str.split("\n");
		const sdpKeywords = ["v=", "o=", "s=", "t=", "m="];

		for (const line of lines) {
			const trimmedLine = line.trim();

			if (sdpKeywords.some(keyword => trimmedLine.startsWith(keyword))) {
				continue;
			} else {
				return false;
			}
		}

		return true;
	}

	function ajax(data, type, callback = false) {
		log("AJAX: " + type);
		//log(data);
		try {
			var xhttp = new XMLHttpRequest();
			xhttp.onreadystatechange = async function () {
				if (this.readyState == 4 && (this.status == 200 || this.status == 201)) {
					var contentType = this.getResponseHeader("content-type");

					if (!contentType.startsWith("application/sdp")) {
						warnlog("The WHIP output destination responded with an incorrect content type; will attempt to continue still.");
					}

					if (!session.whipOut.stats) {
						session.whipOut.stats = {};
					}
					session.whipOut.stats.whipHost = "generic";
					session.whipOut.stats.whep_URL = false;
					session.whipOut.stats.watch_URL = false; // cloudflare and meshcast have this, but meh. aec is an issue, so won't bother for now.
					var WHELPlaybackURL = false; // we will try to determine the WHEP address, if we need to share it with other viewers.

					try {
						if (this.getAllResponseHeaders().indexOf("location") >= 0) {
							session.whipOut.location = this.getResponseHeader("location") || "";
							session.whipOut.location = completeLocationURL(session.whipOutput, session.whipOut.location);
						} else if (!session.whipOut.location && session.whipOutput) {
							session.whipOut.location = session.whipOutput;
							session.whipOut.location = completeLocationURL(session.whipOutput, session.whipOut.location);
						}
					} catch (e) {
						errorlog(e);
					}
					try {
						log(this.getAllResponseHeaders());
						if (this.getAllResponseHeaders().indexOf("whep") >= 0) {
							WHELPlaybackURL = this.getResponseHeader("whep") || false;
						} else {
							console.log("Note: No WHEP key/value was found in the WHIP header response or it was not exposed.\n\nProviding the WHEP URL for this WHIP output via the WHEP header key will allow p2p access to the WHEP stream for others conneted to this peer.");
						}
						if (!WHELPlaybackURL && session.whipOutput) {
							var targetDomain = session.whipOutput.split("/");
							try {
								if (targetDomain.length > 2 && targetDomain[2].endsWith(".cloudflarestream.com") && targetDomain[3].length == 65) {
									WHELPlaybackURL = "https://" + targetDomain[2] + "/" + targetDomain[3].slice(33, 65) + "/webRTC/play";
									session.whipOut.stats.whipHost = "Cloudflare";
								} else if (/^https?:\/\/(?:[\w-]+\.)*meshcast\.io(?:\/|$)/i.test(session.whipOutput)){ // this should be only if meshcast doens't return a whep URL.  we guess as a fallback.
									session.whipOut.stats.whipHost = "Meshcast";
									session.whipOut.stats.watch_URL = "https://meshcast.io/view.html?geo="+session.whipOutput.split("https://")[1].split(".")[0]+"&id="+session.whipOutput.split("meshcast.io/")[1].split("/whip")[0];
								}
							} catch(e){
								errorlog(e);
							}
							
						} else if (WHELPlaybackURL && !(WHELPlaybackURL.startsWith("http://") || WHELPlaybackURL.startsWith("https://"))) {
							var targetDomain = session.whipOutput.split("/");
							if (targetDomain.length > 2) {
								if (WHELPlaybackURL.startsWith("/")) {
									WHELPlaybackURL = targetDomain[0] + "//" + targetDomain[2] + WHELPlaybackURL;
								} else {
									WHELPlaybackURL = targetDomain[0] + "//" + targetDomain[2] + "/" + WHELPlaybackURL;
								}
							}
						} else if (WHELPlaybackURL && /^https?:\/\/(?:[\w-]+\.)*meshcast\.io(?:\/|$)/i.test(WHELPlaybackURL)){ // this should be only if meshcast doens't return a whep URL.  we guess as a fallback.
							try {
								session.whipOut.stats.whipHost = "Meshcast";
								session.whipOut.stats.watch_URL = "https://meshcast.io/view.html?geo="+WHELPlaybackURL.split("https://")[1].split(".")[0]+"&id="+WHELPlaybackURL.split("meshcast.io/")[1].split("/whip")[0];
							} catch(e){
								errorlog(e);
							}
						}
						
						log("WHELPlaybackURL: " + WHELPlaybackURL);
						session.whipOut.stats.whep_URL = WHELPlaybackURL;
					} catch (e) {
						errorlog(e);
					}

					if (WHELPlaybackURL) {
						if (WHELPlaybackURL.includes("|")) {
							var tmp = WHELPlaybackURL.split("|");
							session.whipoutSettings = { type: "whep", url: tmp[0].trim(), token: tmp[1].trim(), started: false};
						} else {
							session.whipoutSettings = { type: "whep", url: WHELPlaybackURL, started: false};
						}
					}

					if (contentType.startsWith("application/sdp") || isSDP(this.responseText)) {
						var jsep = {};
						jsep.sdp = this.responseText;
						jsep.type = "answer";

						try {
							jsep = configureWhipOutSDP(jsep);
						} catch (e) {
							errorlog(e);
						}

						warnlog("Processing answer:");
						warnlog(jsep);

						if (session.whipOut && session.whipOut.location) {
							sessionStorage.setItem("deleteWhipOnLoad", JSON.stringify({ location: session.whipOut.location, whipOutputToken: session.whipOutputToken }));

							session.whipOut.deleteme = function () {
								let xhttp = new XMLHttpRequest();
								if (session.whipOutputToken) {
									xhttp.setRequestHeader("Authorization", "Bearer " + session.whipOutputToken);
								}
								xhttp.onload = function () {
									sessionStorage.removeItem("deleteWhipOnLoad");
								};
								xhttp.onerror = function () {
									sessionStorage.removeItem("deleteWhipOnLoad");
								};
								xhttp.open("DELETE", session.whipOut.location, true);
								xhttp.send();

								delete session.whipOut.deleteme;
							};
						}

						if (session.localNetworkOnly) {
							jsep.sdp = filterSDPLAN(jsep.sdp);
						}
						if (session.stunOnly) { // or whatever flag you want to use
							jsep.sdp = filterStunOnly(jsep.sdp);
						}

						session.whipOut
							.setRemoteDescription(jsep)
							.then(async function () {
								warnlog("SHOULD BE CONNECTED?");
								//var content = "";
								//while (candidates.length) {
								//	var candidate = candidates.pop();
								//	content += candidate.candidate;
								//}
								//warnlog("Content: " + content.length);
								//if (content){
								// warnlog("SENDING TRICKLE"); //.. I should, but I'm not, since most sites don't support it still.
								
								if (session.whipoutSettings){
									session.whipoutSettings.started = Date.now();
								}
								
								if (session.whipOutKeyframe) {
									clearTimeout(session.whipOut.keyframeTimeout);
									session.whipOut.keyframeTimeout = setInterval(function () {
										GOP();
									}, session.whipOutKeyframe); // ensure GOP no longer than 6s
								}
								session.whipOutSetScale();
								
								await sleep(1000); //  give whip server a moment to setup I guess.
								
								if (session.whipoutSettings) {
									for (var UUID in session.pcs) {
										if (session.pcs[UUID].whipout === null) {
											var data = {};
											data.whepSettings = session.whipoutSettings;
											if (session.sendMessage(data, UUID)) {
												session.pcs[UUID].whipout = true;
											}
										}
									}
								}
								//}
							})
							.catch(async function (e) {
								errorlog(e);
								errorlog("Recieved an invalid SDP answer response from the WHIP endpoint. While things may still work, it won't work as intended.");
								if (WHELPlaybackURL) {
									log("Since we have the WHEP URL, we will assume its desired to forward it on, even if it may not work.");
									
									if (session.whipoutSettings){
										session.whipoutSettings.started = Date.now();
									}
									
									try {
										if (session.whipOutKeyframe) {
											clearTimeout(session.whipOut.keyframeTimeout);
											session.whipOut.keyframeTimeout = setInterval(function () {
												GOP();
											}, session.whipOutKeyframe); // ensure GOP no longer than 6s
										}
										session.whipOutSetScale();
									} catch (e) {
										errorlog(e);
									}

									await sleep(1000); //  give whip server a moment to setup I guess.
									
									if (session.whipoutSettings) {
										for (var UUID in session.pcs) {
											if (session.pcs[UUID].whipout === null) {
												var data = {};
												data.whepSettings = session.whipoutSettings;
												if (session.sendMessage(data, UUID)) {
													session.pcs[UUID].whipout = true;
												}
											}
										}
									}
								}
							});
					} else if (contentType == "application/error") {
						if (this.responseText == 432) {
							warnUser("Whip out error: 432");
						} else {
							warnUser("Unknown Whip Out error");
						}
					} else if (callback) {
						callback();
					} else if (WHELPlaybackURL) {
						errorlog("WHEP URL provided in header response, but no SDP answer provided. Will still use the WHEP URL, and hope for the best..");
						if (session.whipoutSettings) {
							
							if (session.whipoutSettings){
								session.whipoutSettings.started = Date.now();
							}
							
							await sleep(1000);
							
							for (var UUID in session.pcs) {
								if (session.pcs[UUID].whipout === null) {
									var data = {};
									data.whepSettings = session.whipoutSettings;
									if (session.sendMessage(data, UUID)) {
										session.pcs[UUID].whipout = true;
									}
								}
							}
						}
					}
				}
			};
			if (type === "trickle-ice-sdpfrag") {
				xhttp.open("PATCH", session.whipOutput, true); // Not supported by most sites yet
			} else {
				xhttp.open("POST", session.whipOutput, true);
			}

			if (session.whipOutputToken) {
				xhttp.setRequestHeader("Authorization", "Bearer " + session.whipOutputToken);
			}

			xhttp.setRequestHeader("Content-Type", "application/" + type);

			xhttp.onerror = function (e) {
				errorlog(e);

				if (window.location.protocol == "https:" && session.whipOutput.startsWith("http://") && !session.whipOutput.startsWith("http://localhost")) {
					console.warn("Mixed HTTP and HTTPS content; this may not work. There are some options, like using localhost, disabling web security in your browser, or using SSL entirely");
					if (!session.cleanOutput) {
						if (window.location.hostname === "vdo.ninja") {
							warnUser("Error: You cannot publish to an HTTP WHIP endpoint from an HTTPS-enabled website.\n\nThere are some possible exceptions and solutions, such as deploying an SSL certificate, hosting from localhost, trying from http://insecure.vdo.ninja, and/or using the Electron Capture app.");
						} else {
							warnUser("Error: You cannot publish to an HTTP WHIP endpoint from an HTTPS-enabled website.");
						}
					}
				} else if (!session.cleanOutput) {
					warnUser("WHIP out failed.\n\nCheck the developer console for possible details.");
				}
			};
			xhttp.send(data);
		} catch (e) {
			errorlog(e);
		}
	}
	function GOP() {
		log("Sending keyframe");
		try {
			if (!session.whipOut) {
				return;
			}

			var senders = session.whipOut.getSenders();
			var sender = false;
			senders.forEach(senderVideo => {
				if (senderVideo.track && senderVideo.track.id && senderVideo.track.kind == "video") {
					sender = senderVideo;
				}
			});

			if (!sender) {
				log("GOP(): can't change bitrate; no video sender found");
				return false;
			}

			var settings = {};
			settings.scaleResolutionDownBy = 10; // 50% of default max

			setEncodings(
				sender,
				settings,
				function (sendr) {
					var settings = {};

					var chromeVersion = getChromiumVersion();
					if (chromeVersion > 80) {
						// just because
						settings.scaleResolutionDownBy = null;
					} else {
						settings.scaleResolutionDownBy = 1.0;
					}

					setEncodings(sendr, settings, function () {
						//log("scaleResolutionDownBy set 3b!");
					});
				},
				sender
			);

			return true;
		} catch (e) {
			errorlog(e);
		}
	}
	whipConnect();
}

function whipClient() {
	// publish to whip.vdo.ninja with obs, to use.  experimental
	if (!session.whipView) {
		return;
	}
	warnlog("WHIP Client started");

	var socket = null;
	var connecting = false;
	var failedCount = 0;

	function connect() {
		clearTimeout(connecting);
		if (socket) {
			if (socket.readyState === socket.OPEN) {
				return;
			}
			try {
				socket.close();
			} catch (e) {}
		}
		log("Trying to load whip websocket...");

		socket = new WebSocket(session.whipServerURL);

		socket.onclose = function () {
			failedCount += 1;
			clearTimeout(connecting);
			connecting = setTimeout(function () {
				connect();
			}, 100 * (failedCount - 1));
		};

		socket.onerror = function (e) {
			console.error(e);
			failedCount += 1;
			clearTimeout(connecting);
			connecting = setTimeout(function () {
				connect();
			}, 100 * failedCount);
		};

		socket.onopen = function () {
			failedCount = 0;
			try {
				var settings = {};
				socket.send(JSON.stringify({ join: session.whipView }));
			} catch (e) {
				connecting = setTimeout(function () {
					connect();
				}, 1);
			}
		};

		socket.addEventListener("message", async function (event) {
			if (event.data) {
				var data = JSON.parse(event.data);

				if ("sdp" in data) {
					try {
						var resp = await processWhipIn(data);
					} catch(e){
						var resp = e && (e.message || e.toString());
					}
					if (resp) {
						var ret = {};
						var get = data.get;
						data = {};
						if (get) {
							data.get = get;
							data.result = resp;
							ret.callback = data;
							socket.send(JSON.stringify(ret));
						}
					}
				} else if (data.type === "candidate") {
					if (data.candidate && data.streamID) {
						await handleIncomingIceCandidate(data);
					}
				} else if (data.type == "delete") {
					warnlog("WHIP publisher is actively disconnecting");
					// session.closeRPC(i, true);
					var ret = {};
					var get = data.get;
					data = {};
					if (get) {
						data.get = get;
						data.result = "OK";
						ret.callback = data;
						socket.send(JSON.stringify(ret));
					}
				} else {
					warnlog("Unsupported incoming data");
				}
			}
		});
	}
	connect();
}
async function processWhipIn(data) {
    // LISTEN FOR REMOTE WHIP (from OBS?)
    var msg = {};
    msg.description = {};
    msg.description.type = "offer";
    msg.description.sdp = data.sdp;
    var UUID = session.generateRandomString(25); // fake
    msg.UUID = UUID;
	
	if (session.forceNoAudioWhipIn || session.forceNoVideoWhipIn){
		try {
			log(msg.description.sdp+"");
			msg.description.sdp = CodecsHandler.modifySdp(msg.description.sdp, session.forceNoAudioWhipIn, session.forceNoVideoWhipIn);
		} catch(e){
			errorlog(e);
		}
	}
	
    if (!msg.description.sdp.includes("a=group:BUNDLE")) { // handling of bundle-only media lines; gstreamer's whipsink 1.25 for example
		try {
			const sdpLines = msg.description.sdp.split('\r\n');
			const bundleLines = sdpLines.filter(line => line.includes('a=mid:'));
			if (bundleLines.length > 0) {
				const mids = bundleLines.map(line => line.split(':')[1]);
				sdpLines.splice(1, 0, `a=group:BUNDLE ${mids.join(' ')}`);
				msg.description.sdp = sdpLines.join('\r\n');
			}
		} catch(e){
			errorlog(e);
		}
    }
	
    if (data.streamID) {
        msg.streamID = data.streamID;
    } else {
        msg.streamID = session.generateRandomString(15); // fake
    }

    log("setupIncoming");
    await session.setupIncoming(msg); // could end up setting up the peer the wrong way.

    session.rpcs[UUID].whip = true;
    var callback = null;
    var promise = new Promise((resolve, reject) => {
        callback = resolve;
    });
    session.rpcs[UUID].whipCallback = callback;

    var callback2 = null;
    var promise2 = new Promise((resolve, reject) => {
        callback2 = resolve;
    });
    session.rpcs[UUID].whipCallback2 = callback2;

    log("CONNECT PEER");
    session.connectPeer(msg);
    log("CONNECT PEER DONE");

    log("ICE BUNDLE PROMISE");
    setTimeout(
        function (UUID) {
            if (session.rpcs[UUID].whipCallback2) {
                session.rpcs[UUID].whipCallback2([...session.rpcs[UUID].iceBundle]);
                clearTimeout(session.rpcs[UUID].iceTimer);
                session.rpcs[UUID].iceTimer = null;
                session.rpcs[UUID].iceBundle = [];
                session.rpcs[UUID].whipCallback2 = null;
            }
        },
        session.whepWait,
        UUID
    );

    var iceBundle = await promise2; // waiting for ICE GATHER COMPLETE; default 2 second. change with &whipwait=2000

    clearTimeout(session.rpcs[UUID].iceTimer);
    session.rpcs[UUID].iceTimer = null;
    session.rpcs[UUID].whipCallback2 = null;

    log("ICE BUNDLE DONE");
    log(iceBundle);

    await promise;
    session.rpcs[UUID].whipCallback = null;

    sdpAnswer = session.rpcs[UUID].localDescription.sdp;
	
	if (session.localNetworkOnly) {
		sdpAnswer = filterSDPLAN(sdpAnswer);
	}
	if (session.stunOnly) { // or whatever flag you want to use
		sdpAnswer = filterStunOnly(sdpAnswer);
	}
	
    //iceBundle.forEach(ice => { // not needed, since the localDescription has it embedded already, since we waited
    //    sdpAnswer += `a=${ice.candidate}\r\n`;
    //});
	
	/* 	
	if (true){ // this code tries to force the TURN server into use, but it's not working that I can see.
	  
	  const sdpLines = sdpAnswer.split('\r\n');
	  const modifiedLines = [];
	  let mediaSection = 0;
	  let candidateAdded = false;
	  let audioPort = null;

	  for (let line of sdpLines) {
		if (line.startsWith('m=')) {
		  mediaSection++;
		  if (mediaSection === 1) {
			// Extract audio port
			audioPort = line.split(' ')[1];
		  } else if (mediaSection === 2 && audioPort) {
			// Set video port to match audio port
			line = `m=video ${audioPort} UDP/TLS/RTP/SAVPF 96`;
		  }
		}

		if (line.startsWith('c=')) {
		  line = `c=IN IP4 51.222.12.223`;
		}

		if (line.startsWith('a=candidate:') && !candidateAdded) {
		  line = `a=candidate:1 1 UDP 2 51.222.12.223 3478 typ relay raddr 0.0.0.0 rport 0`;
		  candidateAdded = true;
		}

		

		modifiedLines.push(line);
	  }

	  return modifiedLines.join('\r\n');
	}
	 */

    return sdpAnswer; // return SDP answer for the remote WHIP request
}
async function handleIncomingIceCandidate(data) {
    const UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === data.streamID);
    if (UUID && session.rpcs[UUID]) {
        try {
            await session.rpcs[UUID].addIceCandidate(new RTCIceCandidate(data.candidate));
            log("Added incoming ICE candidate for stream: " + data.streamID);
        } catch (e) {
            errorlog("Error adding incoming ICE candidate: ", e);
        }
    } else {
        warnlog("Received ICE candidate for unknown stream: " + data.streamID);
    }
}
function processSDPFromServer(sdp) {
	// not the description package; just the sdp
	try {
		if (session.mono && Firefox) {
			// chrome defaults to mono already, but we can force Firefox mono if needed
			sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 0 }, true);
		} else if (Firefox) {
			// Let Firefox be Firefox.. else it might break the server. mediamtx err 400 response if mono
		} else if (session.stereo && session.stereo == 4) {
			// pro audio only when viewing streams
			sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 2 }, true);
		} else if (session.stereo && !session.mono && session.stereo != 3) {
			sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 1 }, true);
		}
	} catch (e) {
		errorlog(e);
	}
	return sdp;
}

function filterIceLAN(candidate) {
	
	if (typeof candidate === 'string') {
        return filterSDPLAN(candidate);
    }
	if (!candidate){return candidate;}
	
	
	try {
		let candidateString = candidate.candidate;
		let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/;
		let isMDNSHostname = candidateString.includes(".local");

		if (!(candidateString.includes("typ host") && (privateIPPattern.test(candidateString) || isMDNSHostname))) {
			log("🟧 dropped candidate due to not being a LAN candidate");
			return false;
		}
		log("🟢 candidate allowed since host-type and has a LAN-IP or is a .local hostname");
	} catch (e) {
		errorlog(e);
	}
	return true;
}
function filterSDPLAN(sdp) {
	try {
		return sdp
			.split("\n")
			.filter(line => {
				if (line.startsWith("a=candidate:")) {
					let parts = line.split(" ");
					let type = parts[7]; // The 'typ' field in the candidate string
					let address = parts[4];
					let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/;
					let isMDNSHostname = address.endsWith(".local");
					log("🟥 dropped candidate from SDP due to not being a LAN candidate");
					return type === "host" && (privateIPPattern.test(address) || isMDNSHostname);
				}
				return true;
			})
			.join("\n");
	} catch (e) {
		errorlog(e);
		return sdp;
	}
}

function filterStunOnlySDP(sdpString) {
    try {
        const lines = sdpString.split('\r\n');
        const filteredLines = lines.filter(line => {
            // If it's a candidate line
            if (line.startsWith('a=candidate:')) {
                // Only keep STUN candidates
                return line.includes(' typ srflx ');
            }
            
            // Keep everything that's not a candidate line
            return true;
        });

        // Process the filtered SDP
        let processedLines = [];
        let currentMedia = null;
        let stunPort = null;
        let stunAddress = null;

        // First pass - find STUN candidate details
        for (const line of filteredLines) {
            if (line.includes(' typ srflx ')) {
                const parts = line.split(' ');
                for (let i = 0; i < parts.length; i++) {
                    if (parts[i] === 'typ' && parts[i+1] === 'srflx') {
                        stunPort = parts[5];
                        stunAddress = parts[4];
                        break;
                    }
                }
            }
        }

        // Second pass - build the SDP with STUN info
        for (const line of filteredLines) {
            if (line.startsWith('m=')) {
                currentMedia = line.split(' ')[0].substr(2);
                if (stunPort && (currentMedia === 'audio' || currentMedia === 'video')) {
                    // Use STUN port for media lines
                    const parts = line.split(' ');
                    parts[1] = stunPort;
                    processedLines.push(parts.join(' '));
                } else {
                    processedLines.push(line);
                }
            } else if (line.startsWith('c=') && stunAddress) {
                // Use STUN address for connection lines
                processedLines.push(`c=IN IP4 ${stunAddress}`);
            } else {
                processedLines.push(line);
            }
        }

        return processedLines.join('\r\n');
    } catch (e) {
        errorlog(e);
        return sdpString; // Return original on error
    }
}

// If you need to modify the individual candidate filter as well:
function filterStunOnly(candidate) {
    if (typeof candidate === 'string') {
        return filterStunOnlySDP(candidate);
    }
	if (!candidate){return candidate;}
    console.log(candidate);
    try {
        let candidateString = candidate.candidate;
        
        // Only allow STUN candidates
        if (!candidateString.includes("typ srflx")) {
            log("🟧 Dropped non-STUN candidate");
            return false;
        }
        
        // Make sure it's not containing private IP ranges
        let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/;
        if (privateIPPattern.test(candidateString)) {
            log("🟧 Dropped STUN candidate with private IP");
            return false;
        }
        
        log("🟢 Allowed STUN candidate");
        return true;
    } catch (e) {
        errorlog(e);
        return false;
    }
}
async function whepIn(whepInput = false, whepInputToken = false, UUID = false) {
	// PLAY WHEP; will continually bring it in, and retry continuously
	var candidates = [];
	var responseLocation = false;
	var acceptPatch = false;
	var eTag = false;
	var icePwd = false;
	var iceUfrag = false;
	// var reconnect = null;
	//var maxRetries = 5;
	//var delay = 2000;


	if (!UUID) {
		UUID = "whep_" + session.generateRandomString(25); // fake
	}

	whepInput = whepInput || session.whepInput;
	if (!whepInput) {
		errorlog("no whepInput");
		return;
	}
	whepInputToken = whepInputToken || session.whepInputToken;

	async function whepConnect() {
		//return new Promise((resolve, reject) => {
		try {
			if (!(UUID in session.rpcs)) {
				session.rpcs[UUID] = {};
				session.rpcs[UUID].stats = {};
				session.rpcs[UUID].allowGraphs = false;
				session.rpcs[UUID].allowDrawing = false;
				session.rpcs[UUID].inboundAudioPipeline = {};
				session.rpcs[UUID].channelOffset = false;
				session.rpcs[UUID].channelWidth = false;
				session.rpcs[UUID].settings = false;
				session.rpcs[UUID].defaultSpeaker = false;
				session.rpcs[UUID].lockedVideoBitrate = false; // doesn't do anything
				session.rpcs[UUID].lockedAudioBitrate = false;
				session.rpcs[UUID].manualBandwidth = false; // doesn't do anything, except maybe help keep track of pause/play states
				session.rpcs[UUID].motionDetectionInterval = false;
				session.rpcs[UUID].buffer = false;
				session.rpcs[UUID].getStatsTimeout = null;
			}

			if (!session.configuration) {
				await chooseBestTURN();
			}

			if (session.encodedInsertableStreams) {
				// most servers won't support this
				session.configuration.encodedInsertableStreams = true;
			}

			var config = { ...session.configuration };

			// if (whepInput.includes("cloudflare")){
			//		config.iceTransportPolicy = "relay"; // oof.  Doesn't work with Cloudflare without this? REVISIT  (Update: I guess they fixed it? Oct 23rd its working without it)
			// }

			try {
				session.rpcs[UUID].whep = new RTCPeerConnection(config);
			} catch (err) {
				errorlog(err);
				if (!session.cleanOutput) {
					warnUser("An RTC error occured");
				}
			}

			var video = true;
			var audio = true;

			if (session.novideo !== false && !session.novideo.includes(session.rpcs[UUID].streamID)) {
				video = false;
			} else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.video) {
				video = false;
			}
			if (session.noaudio !== false && !session.noaudio.includes(session.rpcs[UUID].streamID)) {
				audio = false;
			} else if (session.excludeaudio && session.excludeaudio.includes(session.rpcs[UUID].streamID)) {
				audio = false;	
			} else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.audio) {
				audio = false;
			}

			if (!audio && !video) {
				errorlog("We will not request the whep source as no audio or video is requested");
				return;
			}
			
			if (video){
				disableQualityDirector(UUID);
			}

			if (!session.manual || !session.director) {
				window.onresize = updateMixer;
				window.onorientationchange = function () {
					setTimeout(updateMixer, 200);
				};
			}

			try {
				if (video) {
					session.rpcs[UUID].whep.addTransceiver("video", { direction: "recvonly" });
				}
				if (audio) {
					session.rpcs[UUID].whep.addTransceiver("audio", { direction: "recvonly" });
				}
			} catch (e) {
				errorlog(e);
			}

			session.rpcs[UUID].whep.ontrack = function (event) {
				warnlog("TRACK INBOUND!");
				warnlog(event);
				session.onTrack(event, UUID);
				// maxRetries = 5; // reset allowed reconnection limit
				let track = null;
				if (event.streams && event.streams[0]) {
					try {
						let newStream = event.streams[0];
						track = newStream.getVideoTracks()[0];
					} catch(e){}
				} else if (event.track && event.track.kind && (event.track.kind=="video")) {
					track = event.track
				}
				if (track){
					log(track);
					setTimeout(function(track,UUID){
						if (session.rpcs[UUID] && track && track.id){
							if (session.rpcs[UUID].stats && session.rpcs[UUID].stats[track.id] && ("keyFramesRequested_pli" in session.rpcs[UUID].stats[track.id])){
								// TODO ?? maybe not,b ut console.log("I need to request a keyframe. keyFramesRequested_pli: "+session.rpcs[UUID].stats[track.id].keyFramesRequested_pli);
							}
						}
					},6100,track,UUID); // 3 seconds for stats to update + 2 second for keyframe to trigger + 1100 for delay
				}
				
				// IF track is a video..
				// wait to see if a keyframe is received within 2 seconds
				// if not keyframe, request one.
				// session.whipOutKeyframeOnNewViewer
				// session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0;
				/* if ("pliCount" in stat) {
					data.stats.total_pli_count = stat.pliCount;
				}

				if ("keyFramesEncoded" in stat) {
					data.stats.total_key_frames_encoded = stat.keyFramesEncoded;
				} */
			};
		} catch (err) {
			errorlog(err);
			if (!session.cleanOutput) {
				warnUser("An RTC error occured");
			}
		}

		session.rpcs[UUID].whep.onnegotiationneeded = requestStream; // bug: https://groups.google.com/forum/#!topic/discuss-webrtc/3-TmyjQ2SeE
		
		session.rpcs[UUID].whep.oniceconnectionstatechange = function() {
             if (session.rpcs[UUID] && session.rpcs[UUID].whep && (session.rpcs[UUID].whep.iceConnectionState === 'disconnected' || 
                session.rpcs[UUID].whep.iceConnectionState === 'failed')) {
                console.warn("ICE connection failed or disconnected");
                retryWhepConnection(UUID);
            }
        };

        session.rpcs[UUID].whep.onconnectionstatechange = function() {
            if (session.rpcs[UUID] && session.rpcs[UUID].whep && session.rpcs[UUID].whep.connectionState === 'failed') {
                console.warn("Whep connection failed");
                retryWhepConnection(UUID);
            }
        };

		session.rpcs[UUID].whep.onicecandidate = function (event) {
			if (!session.rpcs[UUID] || !session.rpcs[UUID].whep){
				return;
			}
			
			if (event.candidate == null) {
				warnlog("END OF ICE CANDIDATES");
				if (session.rpcs[UUID].whep.iceCompletedCallback) {
					session.rpcs[UUID].whep.iceCompletedCallback();
				}
				return;
			} else if (eTag && icePwd && iceUfrag && acceptPatch && acceptPatch == "application/trickle-ice-sdpfrag" && event.candidate && responseLocation && !session.rpcs[UUID].whep.iceCompletedCallback) {
				// "left over" candidates not sent with the SDP offer
				log("Send patch request with ice candidate");

				try {
					if (session.localNetworkOnly) {
						if (!filterIceLAN(event.candidate)) {
							return;
						}
					}
					if (session.stunOnly) { // or whatever flag you want to use
						if (!filterStunOnly(event.candidate)) {
							return;
						}
					}
				} catch (e) {
					errorlog(e);
				}

				if (event.candidate.candidate) {
					let patchCandidate =
						"a=ice-ufrag:" +
						iceUfrag +
						"\r\n" + // <== what a mess..  https://datatracker.ietf.org/doc/html/draft-murillo-whep
						"a=ice-pwd:" +
						icePwd +
						"\r\n" +
						"m=audio 9 RTP/AVP 0\r\n" +
						"a=mid:0\r\n" +
						"a=" +
						event.candidate.candidate +
						"\r\n" +
						"a=end-of-candidates\r\n";

					// a=ice-ufrag:EsAw
					// a=ice-pwd:P2uYro0UCOQ4zxjKXaWCBui1
					// m=audio RTP/AVP 0
					// a=mid:0
					// a=candidate:1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1
					// a=candidate:3471623853 1 udp 2122194687 198.51.100.1 61765 typ host generation 0 ufrag EsAw network-id 2
					// a=candidate:473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1
					// a=candidate:2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2
					// a=end-of-candidates

					// If-Match: "38sdf4fdsf54:EsAw"
					ajax(patchCandidate, "trickle-ice-sdpfrag", false, { "if-match": eTag });
				}
			} else {
				try {
					if (session.localNetworkOnly) {
						if (!filterIceLAN(event.candidate)) {
							return;
						}
					}
					if (session.stunOnly) { // or whatever flag you want to use
						if (!filterStunOnly(event.candidate)) {
							return;
						}
					}
				} catch (e) {
					errorlog(e);
				}

				candidates.push(event.candidate); // send later if I can?
			}
			//log(event.candidate);
		};

		log("onnegotiationneeded event setup");
	}
	
	function retryWhepConnection(UUID) {
		if (!session.rpcs[UUID]) {
			log("Session closed, stopping retry attempts");
			return;
		}

		if (session.rpcs[UUID].reconnecting) {
			return;
		}
		session.rpcs[UUID].reconnecting = true;

		const maxRetries = 5;
		const initialDelay = 2000;
		const maxDelay = 20000;

		let currentRetry = 0;
		let currentDelay = initialDelay;

		function attemptReconnect() {
			if (!session.rpcs[UUID]) {
				log("Session closed during retry, stopping attempts");
				return;
			}

			if (session.rpcs[UUID].whep &&
				(session.rpcs[UUID].whep.connectionState === 'connected' ||
				 session.rpcs[UUID].whep.iceConnectionState === 'connected' ||
				 session.rpcs[UUID].whep.iceConnectionState === 'completed')) {
				log("WHEP connection is already established. No need to reconnect.");
				session.rpcs[UUID].reconnecting = false;
				return;
			}

			log(`Attempting WHEP reconnection (attempt ${currentRetry + 1}/${maxRetries})`);

			if (session.rpcs[UUID].whep && session.rpcs[UUID].whep.close) {
				session.rpcs[UUID].whep.close();
			}
			
			try {
				if (session.rpcs[UUID].videoElement && "recorder" in session.rpcs[UUID].videoElement) {
					session.rpcs[UUID].videoElement.recorder.stop();
				}
			} catch (e) {
				warnlog(e);
			}
			
			try {
				if (session.rpcs[UUID].streamSrc) {
					session.rpcs[UUID].streamSrc.getTracks().forEach(function (track) {
						track.stop();
						log("Track stopped");
					});
					session.rpcs[UUID].streamSrc = null;
				}
			} catch (e) {}
			
			try {
				if (!session.rpcs[UUID].UUID && document.getElementById("container_" + UUID)) {
					getById("container_" + UUID).parentNode.removeChild(getById("container_" + UUID));
					updateLockedElements();
				}
			} catch (e) {
				warnlog(e);
			}

			try {
				if (session.rpcs[UUID].videoElement) {
					session.rpcs[UUID].videoElement.remove();
					session.rpcs[UUID].videoElement = null;
				}
				if (session.rpcs[UUID].canvas) {
					session.rpcs[UUID].canvas.remove();
				}
				if (session.rpcs[UUID].imageElement) {
					session.rpcs[UUID].imageElement.remove();
				}
				if ("eventPlayActive" in session.rpcs[UUID]) {
					clearInterval(session.rpcs[UUID].eventPlayActive);
				}
				
				if (!session.director || session.switchMode) {
					setTimeout(function () {
						updateMixer();
					}, 1);
				}
			} catch (e) {
				warnlog(e);
			}

			whepConnect(UUID).then(() => {
				log("WHEP reconnection successful");
				session.rpcs[UUID].reconnecting = false;
			}).catch(error => {
				warnlog("WHEP reconnection failed:", error);
				currentRetry++;
				if (currentRetry < maxRetries) {
					currentDelay = Math.min(currentDelay * 2, maxDelay);
					log(`Retrying in ${currentDelay}ms`);
					setTimeout(attemptReconnect, currentDelay);
				} else {
					log("Max retries reached, stopping reconnection attempts");
					session.rpcs[UUID].reconnecting = false;
					// Implement user feedback here
				}
			});
		}

		attemptReconnect();
	}

	var requestingStream = false;
	function requestStream(event) {
		if (requestingStream) {
			log(event);
			errorlog("onnegotiationneeded again?");
			return;
		}
		requestingStream = true;
		warnlog("ON NEGO NEEDED");
		warnlog(event);

		try {
			session.rpcs[UUID].whep
				.createOffer()
				.then(async function (offer) {
					if (session.localNetworkOnly) {
						offer.sdp = filterSDPLAN(offer.sdp);
					}
					if (session.stunOnly) { // or whatever flag you want to use
						offer.sdp = filterStunOnly(offer.sdp);
					}

					offer.sdp = processSDPFromServer(offer.sdp);
					//offer.sdp = CodecsHandler.setOpusAttributes(offer.sdp, {'stereo': 1});
					return session.rpcs[UUID].whep.setLocalDescription(offer);
				})
				.then(async function () {
					//log(session.rpcs[UUID].whep.localDescription);

					try {
						if (session.whepWait) {
							log("Waiting for ice candidates to collect. At least 300ms recommended; at most 30-seconds.");
							let startTime = Date.now();
							const { promise, resolve } = sleepCancellable(session.whepWait); // 2000ms default; I want to give the ICE / STUN / TURN time to collect.
							session.rpcs[UUID].whep.iceCompletedCallback = resolve; // Can complete earlier if possible.
							await promise; // pausing for a moment; until all collected or timed out
							log("Finished waiting for ice candidates. Waited " + (Date.now() - startTime) / 1000 + "-seconds");
							delete session.rpcs[UUID].whep.iceCompletedCallback;
						}
					} catch (e) {
						errorlog(e);
					}

					// candidates = []; // clear collected candidates so far, as they are part of the localDescription's footer probably
					var sdp = session.rpcs[UUID].whep.localDescription.sdp;
					sdp = processSDPFromServer(sdp);

					//description.sdp = processSDPFromServer(description.sdp); // cause server to not send
					if (sdp.includes("sendrecv")) {
						errorlog("Should not include sendrecv");
						sdp = sdp.replace("a=sendrecv", "a=recvonly");
						sdp = sdp.replace("v=sendrecv", "v=recvonly");
					}
					ajax(sdp, "sdp");
				})
				.catch(function (err) {
					requestingStream = false;
				});
		} catch (e) {
			requestingStream = false;
			errorlog(e);
		}
	}

	function ajax(dataPayload, type, callback = false, headers = false) {
		// https://datatracker.ietf.org/doc/html/draft-murillo-whep
		//log(dataPayload);
		try {
			var xhttp = new XMLHttpRequest();
			xhttp.onreadystatechange = function () {
				if (this.readyState == 4 && (this.status == 200 || this.status == 201)) {
					try {
						// 200 not in spec (meant to be an options response), but I want to be flexible

						let headers = xhttp.getAllResponseHeaders();

						var contentType = false;
						if (headers.indexOf("content-type") >= 0) {
							contentType = this.getResponseHeader("content-type");
						}
						if (headers.indexOf("location") >= 0) {
							responseLocation = this.getResponseHeader("location");
						}
						if (headers.indexOf("accept-patch") >= 0) {
							acceptPatch = this.getResponseHeader("accept-patch");
						}
						if (headers.indexOf("etag") >= 0) {
							eTag = this.getResponseHeader("etag");
						}

						if (responseLocation && !(responseLocation.startsWith("http://") || responseLocation.startsWith("https://"))) {
							let requestURL = new URL(whepInput); // Replace 'yourRequestURL' with the URL you posted to.
							let protocol = requestURL.protocol;
							let hostname = requestURL.hostname;
							let port = requestURL.port || (protocol === "https:" ? "443" : "80"); // Default port based on protocol
							responseLocation = `${protocol}//${hostname}:${port}${responseLocation}`;
						}

						if (contentType && contentType.startsWith("application/sdp")) {
							var description = {};
							description.sdp = this.responseText;
							description.type = "answer";

							warnlog("Processing answer:");

							iceUfrag = description.sdp.match(/a=ice-ufrag:(.*)\r\n/);
							if (iceUfrag) {
								iceUfrag = iceUfrag[1];
							}
							icePwd = description.sdp.match(/a=ice-pwd:(.*)\r\n/);
							if (icePwd) {
								icePwd = icePwd[1];
							}

							if (session.localNetworkOnly) {
								description.sdp = filterSDPLAN(description.sdp);
							}
							if (session.stunOnly) { // or whatever flag you want to use
								description.sdp = filterStunOnly(description.sdp);
							}

							description.sdp = processSDPFromServer(description.sdp); // setup stereo/mono
							session.rpcs[UUID].whep
								.setRemoteDescription(description)
								.then(function () {
									warnlog("SHOULD BE CONNECTED?");
									requestingStream = false;
								})
								.catch(function (e) {
									log(e);
									requestingStream = false;
								});

							// the request is done, but lets handle any old ice candidates
							if (eTag && icePwd && iceUfrag && acceptPatch && acceptPatch == "application/trickle-ice-sdpfrag" && candidates.length && responseLocation && !session.rpcs[UUID].whep.iceCompletedCallback) {
								// "left over" candidates not sent with the SDP offer
								log("Send patch request with ice candidates");
								let patchCandidates =
									"a=ice-ufrag:" +
									iceUfrag +
									"\r\n" + // <== what a mess..  https://datatracker.ietf.org/doc/html/draft-murillo-whep
									"a=ice-pwd:" +
									icePwd +
									"\r\n" +
									"m=audio 9 RTP/AVP 0\r\n" + // if I leave out the port (9), then MediaMTX breaks, but this is not in the draft spec as linked above
									"a=mid:0\r\n";
								candidates.forEach(candidate => {
									patchCandidates += "a=" + candidate.candidate + "\r\n";
								});
								candidates = [];
								patchCandidates += "a=end-of-candidates\r\n";
								//
								// If-Match: "38sdf4fdsf54:EsAw"
								ajax(patchCandidates, "trickle-ice-sdpfrag", false, { "if-match": eTag });
							} else {
								warnlog("Trickling candidates via PATCH requests not supported it seems");
							}
						} else if (contentType == "application/error") {
							if (!session.cleanOutput) {
								warnUser("Unknown WHEP playback error");
							}
						} else if (callback) {
							callback();
						}
					} catch(e){
						requestingStream = false;
					}
				} else if (this.readyState == 4 && this.status == 204) {
					requestingStream = false;
					if (type == "trickle-ice-sdpfrag") {
						// patch candidate request accepted?
					} else {
						// not in spec?
					}
				} else if (this.readyState == 4){
					requestingStream = false;
					// console.warn(this.status, this.readyState);
					if (this.status == 432) {
					} else if (this.status == 405) {
						// GET, HEAD or PUT not allowed atm
					} else if (this.status == 501) {
					} else if (this.status == 412) {
						// etag trickle did not match
					}
					if (!this.status || this.status >=400){
						if (type === "sdp") {
							retryWhepConnection(UUID);
						}
					}
				}
			};
			if (type === "trickle-ice-sdpfrag") {
				if (responseLocation) {
					xhttp.open("PATCH", responseLocation, true);
				} else {
					xhttp.open("PATCH", whepInput, true); // cause who knows. worth trying?
				}
			} else {
				xhttp.open("POST", whepInput, true);
			}
			xhttp.setRequestHeader("Content-Type", "application/" + type);

			if (whepInputToken) {
				xhttp.setRequestHeader("Authorization", "Bearer " + whepInputToken); // spam it on every request; fail safe
			}

			if (headers) {
				Object.keys(headers).forEach(key => {
					xhttp.setRequestHeader(key, headers[key]);
				});
			}
			xhttp.onerror = function (e) {
				errorlog(e);
				requestingStream = false;
				if (!session.cleanOutput) {
					if (whepInput.startsWith("https://")) {
						if (location.protocol !== "https:") {
							warnUser("WHEP playback failed.\n\nThe website needs to be loaded via https (ssl) to access media devices.");
						} else if ("isSecureContext" in window && window.isSecureContext === false) {
							warnUser("WHEP playback failed.\n\nThe website may have assets loaded in an insecure context.");
						} else {
							retryWhepConnection(UUID);
						}
					} else {
						// vdo.ninja itself is secure
						if (location.protocol === "https:") {
							if (location.hostname == "vdo.ninja") {
								warnUser("WHEP playback failed.\n\nThe WHEP URL needs to be using https if from an SSL-enabled website.\n\nPerhaps try using <a href='http://insecure.vdo.ninja" + location.pathname + location.search + "'>http://insecure.vdo.ninja<a> instead.", false, false);
							} else {
								warnUser("WHEP playback failed.\n\nThe WHEP URL needs to be using https if from an SSL-enabled website.");
							}
						} else if ("isSecureContext" in window && window.isSecureContext === false) {
							warnUser("WHEP playback failed.\n\nThe website may have assets loaded in a secure context.");
						} else {
							retryWhepConnection(UUID);
						}
					}
				}
			};

			xhttp.send(dataPayload);
		} catch (e) {
			requestingStream = false;
			errorlog(e);
		}
	}

	whepConnect();
	return UUID;
}
////////
function whepOut() {
	// publish to whep.vdo.ninja with obs, to use.  experimental
	if (!session.whepHost) {
		return;
	}
	warnlog("WHEP Client started");

	var socket = null;
	var connecting = false;
	var failedCount = 0;

	function connect() {
		clearTimeout(connecting);
		if (socket) {
			if (socket.readyState === socket.OPEN) {
				return;
			}
			try {
				socket.close();
			} catch (e) {}
		}
		log("Trying to load whep websocket...");

		socket = new WebSocket("wss://whep.vdo.ninja");

		socket.onclose = function () {
			failedCount += 1;
			clearTimeout(connecting);
			connecting = setTimeout(function () {
				connect();
			}, 100 * (failedCount - 1));
		};

		socket.onerror = function (e) {
			console.error(e);
			failedCount += 1;
			clearTimeout(connecting);
			connecting = setTimeout(function () {
				connect();
			}, 100 * failedCount);
		};

		socket.onopen = function () {
			failedCount = 0;
			try {
				var settings = {};
				socket.send(JSON.stringify({ join: session.whepHost }));
			} catch (e) {
				connecting = setTimeout(function () {
					connect();
				}, 1);
			}
		};

		socket.addEventListener("message", async function (event) {
			if (event.data) {
				log(event.data);
				var data = JSON.parse(event.data);

				if ("sdp" in data) {
					var resp = await processWHEPout(data);
					if (resp) {
						var ret = {};
						var get = data.get;
						data = {};
						if (get) {
							data.get = get;
							data.result = resp;
							ret.callback = data;
							log(ret);
							socket.send(JSON.stringify(ret));
						}
					}
				} else if (data.type == "delete") {
					warnlog("WHIP Client is actively disconnecting");
					// session.closeRPC(i, true);
					var ret = {};
					var get = data.get;
					data = {};
					if (get) {
						data.get = get;
						data.result = "OK";
						ret.callback = data;
						log(ret);
						socket.send(JSON.stringify(ret));
					}
				}
			}
		});
	}
	connect();
}

async function processWHEPout(data) {
	// LISTEN FOR REMOTE WHIP

	var description = {};
	description.type = "offer";

	var isGstreamer = false;
	description.sdp = data.sdp.replace(/a=rtpmap:111 OPUS\/48000\r\n/g, "a=rtpmap:111 opus/48000/2\r\n"); // gstreamer fix
	if (description.sdp !== data.sdp) {
		isGstreamer = true; // ugh. i'll need to revisit when gstreamer/whepsrc improves.
	}

	var UUID = session.generateRandomString(25) + "_whepout"; // client side made up; just needs to be unique is all;

	if (UUID in session.pcs) {
		UUID = session.generateRandomString(25) + "_whepout"; // ha, pretty pointless.
	}

	try {
		if (!session.configuration) {
			await chooseBestTURN();
		}
		if (session.encodedInsertableStreams) {
			// most servers won't support this
			session.configuration.encodedInsertableStreams = true;
		}
		var config = { ...session.configuration };

		session.pcs[UUID] = new RTCPeerConnection(config);

		session.pcs[UUID].onicecandidate = event => {
			if (event.candidate) {
				// Handle ICE candidate.. or not.
			}
			// try {
			// if (session.localNetworkOnly) {
			//if (session.localNetworkOnly){
			//	if (!filterIceLAN(event.candidate)){
			//		return;
			//		}
			//	}
			// }
			// } catch(e) {errorlog(e);}
		};

		if (session.localNetworkOnly) {
			description.sdp = filterSDPLAN(description.sdp);
		}
		if (session.stunOnly) { // or whatever flag you want to use
			description.sdp = filterStunOnly(description.sdp);
		}

		try {
			await session.pcs[UUID].setRemoteDescription(description);
		} catch (e) {
			errorlog(e);
			errorlog("If you are seeing this error, the browser likely isn't able to match what you are requesting. Compare a normal browser-create SDP with what you are offering.");
		}

		const transceivers = session.pcs[UUID].getTransceivers();

		transceivers.forEach(transceiver => {
			const direction = transceiver.currentDirection || transceiver.direction;
			// Set to sendonly or sendrecv if not already set and if there is a local track to send
			if (direction !== "sendrecv" && direction !== "sendonly") {
				transceiver.direction = "sendonly"; // or 'sendrecv' if you also want to receive
			} else {
				return;
			}
			// Assuming you have a track to send
			session.videoElement.srcObject.getAudioTracks().forEach(track => {
				if (transceiver.direction.includes("send")) {
					if (transceiver.sender.track && transceiver.sender.track.kind != "audio") {
						return;
					} else if (transceiver.receiver.track && transceiver.receiver.track.kind != "audio") {
						return;
					}

					transceiver.sender
						.replaceTrack(track)
						.then(() => {
							log(`Added track: ${track.kind}`);
						})
						.catch(errorlog);
				}
			});

			session.videoElement.srcObject.getVideoTracks().forEach(track => {
				if (transceiver.direction.includes("send")) {
					if (transceiver.sender.track && transceiver.sender.track.kind != "video") {
						return;
					} else if (transceiver.receiver.track && transceiver.receiver.track.kind != "video") {
						return;
					}

					transceiver.sender
						.replaceTrack(track)
						.then(() => {
							log(`Added track: ${track.kind}`);
						})
						.catch(errorlog);
				}
			});
		});

		//await sleep(300); // give it time to collect ice candidates. too lazy to do a promise callback right now

		const localDescription = await session.pcs[UUID].createAnswer();

		if (session.localNetworkOnly) {
			localDescription.sdp = filterSDPLAN(localDescription.sdp);
		}
		
		if (session.stunOnly) { // or whatever flag you want to use
			localDescription.sdp = filterStunOnly(localDescription.sdp);
		}
		

		await session.pcs[UUID].setLocalDescription(localDescription);

		await sleep(session.whepWait);

		if (isGstreamer) {
			return session.pcs[UUID].localDescription.sdp.replace("a=rtpmap:111 opus/48000/2\r\n", "a=rtpmap:111 OPUS/48000\r\n"); // not sure if this makes sense tho.
		}

		return session.pcs[UUID].localDescription.sdp;
		//}
	} catch (error) {
		console.error("Error setting up the peer connection:", error);
	}
}
/////
function pokePostAPI(action, data, streamID) {
	var msg = {};
	msg.update = {};
	msg.update.streamID = streamID || session.streamID || null;
	msg.update.action = action;
	msg.update.value = data;

	try {
		var xhttp = new XMLHttpRequest();
		xhttp.onreadystatechange = function () {
			if (this.readyState == 4 && (this.status == 200 || this.status == 201)) {
				log("good");
			} else {
				warnlog("post api didn't work?");
			}
		};
		xhttp.open("POST", session.postApi, true);
		xhttp.setRequestHeader("Content-type", "application/json");

		xhttp.onerror = function (e) {
			errorlog(e);
		};
		xhttp.send(JSON.stringify(msg));
	} catch (e) {
		errorlog(e);
	}
}

var queuedSendingAPIMsgs = [];
function pokeAPI(action, data, streamID = null) {
	if (session.postApi) {
		pokePostAPI(action, data, streamID);
	}

	if (!session.api) {
		return;
	}

	if (session.apiSocket) {
		try {
			var msg = {};
			msg.update = {};
			msg.update.streamID = streamID || session.streamID || null;
			msg.update.action = action;
			msg.update.value = data;
			session.apiSocket.send(JSON.stringify(msg));
		} catch (e) {
			errorlog(e);
		}
	} else if (session.apiSocket !== null) {
		queuedSendingAPIMsgs.push([action, data, streamID]);
		if (queuedSendingAPIMsgs.length > 20) {
			queuedSendingAPIMsgs.shift();
		}
	}
}

function pokeDiscord(action, data={}) {
	if (!session || !session.discordHook){return;}
    const { streamID, label, ses, hangup, startTime } = data;
    if (hangup && !session.discordHookSensitive) return;
    const duration = Math.floor((Date.now() - (startTime || 0)) / 1000);
    const message = {
        embeds: [{
            title: "Incoming stream disconnected unexpectedly and it didn't reconnect in 60s",
            color: 0xFF0000,
            fields: [
                { name: "Incoming Stream ID", value: streamID || "Unknown", inline: true },
                { name: "Duration of connection", value: `${duration}s`, inline: true }
            ],
            timestamp: new Date().toISOString()
        }]
    };
    if (label) message.embeds[0].fields.push({ name: "Viewer", value: label, inline: true });
    if (session.label) message.embeds[0].fields.push({ name: "Publisher", value: session.label, inline: true });
    setTimeout(() => {
        // Check if stream is reconnected by looking through all RPCs
        const isReconnected = Object.values(session.rpcs || {}).some(rpc => rpc.streamID === streamID);
        if (!isReconnected) {
            fetch(session.discordHook, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(message)
            }).catch(err => console.error('Discord webhook failed:', err));
        }
    }, 60000);
}


function getGuestTarget(type, id) {
	var element = document.querySelector('[data-sid="' + id + '"][data-action-type="' + type + '"], [data-sid="' + id + '"] [data-action-type="' + type + '"]'); // data-sid="P5MQpia"
	if (!element) {
		return getRightOrderedElement('[data--u-u-i-d] [data-action-type="' + type + '"]', id);
	}
	return element;
}

function getGuestTargetScene(scene, id) {
	var element = document.querySelector('[data-action-type="addToScene"][data-scene="' + scene + '"][data-sid="' + id + '"], [data-sid="' + id + '"] [data-action-type="addToScene"][data-scene="' + scene + '"]'); // data-sid="P5MQpia"
	if (!element) {
		return getRightOrderedElement('[data-action-type="addToScene"][data-scene="' + scene + '"][data--u-u-i-d]', id);
	}
	return element;
}
function getGuestTargetGroup(group, id) {
	var element = document.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"][data-sid="' + id + '"], [data-sid="' + id + '"] [data-action-type="toggle-group"][data-group="' + group + '"]'); // data-sid="P5MQpia"
	if (!element) {
		return getRightOrderedElement('[data-action-type="toggle-group"][data-group="' + group + '"][data--u-u-i-d]', id);
	}
	return element;
}

async function targetGuest(target, action, value = null, value2 = null) {
	if (target) {
		if ((target == (parseInt(target) + "")) && (target < 100)) {
			target -= 1;
		}
	} else {
		target = 1;
	}
	warnlog("target " + target);
	warnlog("action " + action);
	warnlog("value " + value);
	if ((action == 0) || (action == "forward") || (action == "transfer")){
		var element = getGuestTarget("forward", target);
		if (element) {
			return await directMigrate(element, true, value); // if value is set, it will auto transfer the guest to that room.
		} else {
			return false;
		}
	} else if ((action == 1) || (action == "addScene")) {
		var scene = 1;
		if (value == "null" || value == null || value == "toggle") {
			scene = 1;
		} else if (value !== true && value !== false) {
			scene = value;
		}
		var element = getGuestTargetScene(scene, target); // oscid/action/target/value   1/1/scene
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directEnable(element, true); // false or true return
		}
	} else if (action == 2 || action == "muteScene") {
		var element = getGuestTarget("mute-scene", target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directMute(element, true); // false/true
		}
	} else if (action == 3 || action == "mic" || action == "audio") {
		var element = getGuestTarget("mute-guest", target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return remoteMute(element, true); // false/true
		}
	} else if (action == 4 || action == "hangup") {
		var element = getGuestTarget("hangup", target);
		if (element) {
			return directHangup(element, true); // false or true; false if confirmed no
		}
	} else if (action == 5 || action == "soloChat" || action == "soloTalk") {
		// see soloChatBidirectional action=9 for two-way
		var element = getGuestTarget("solo-chat", target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return session.toggleSoloChat(element.dataset.UUID);
		}
	} else if (action == 6 || action == "speaker") {
		var element = getGuestTarget("toggle-remote-speaker", target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return remoteSpeakerMute(element);
		}
	} else if (action == 7 || action == "display") {
		var element = getGuestTarget("toggle-remote-display", target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return remoteDisplayMute(element);
		}
	} else if (action == 8 || action == "group") {
		if (value == "null" || value == null) {
			value = 1;
		}
		var element = getGuestTargetGroup(value, target);
		if (element) {
			return changeGroup(element, null, value);
		}
	} else if (action == 9 || action == "soloChatBidirectional" || action == "soloTalkBidirectional") {
		var element = getGuestTarget("solo-chat", target);
		if (element) {
			var ctrl = {};
			ctrl.ctrlKey = true;
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return session.toggleSoloChat(element.dataset.UUID, ctrl);
		}
	} else if (action == 10 || action == "video" || action == "camera") {
		var element = getGuestTarget("mute-video-guest", target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return remoteMuteVideo(element, true); // false/true
		}
	} else if (action == 12 || action == "addScene2") {
		var element = getGuestTargetScene(2, target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directEnable(element, true);
		}
	} else if (action == 13 || action == "addScene3") {
		var element = getGuestTargetScene(3, target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directEnable(element, true);
		}
	} else if (action == 14 || action == "addScene4") {
		var element = getGuestTargetScene(4, target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directEnable(element, true);
		}
	} else if (action == 15 || action == "addScene5") {
		var element = getGuestTargetScene(5, target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directEnable(element, true);
		}
	} else if (action == 16 || action == "addScene6") {
		var element = getGuestTargetScene(6, target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directEnable(element, true);
		}
	} else if (action == 17 || action == "addScene7") {
		var element = getGuestTargetScene(7, target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directEnable(element, true);
		}
	} else if (action == 18 || action == "addScene8") {
		var element = getGuestTargetScene(8, target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return directEnable(element, true);
		}
	} else if (action == 19 || action == "forceKeyframe") {
		var element = getGuestTarget("force-keyframe", target);
		if (element) {
			return requestKeyframeScene(element);
		}
	} else if (action == 20 || action == "soloVideo") {
		var element = getGuestTarget("solo-video", target);
		if (element) {
			if (value === true) {
				element.value = 1;
			} else if (value === false) {
				element.value = 0;
			}
			return requestInfocus(element);
		}
	} else if (action == 21 || action == "sendChat") {
		var element = getGuestTarget("solo-video", target); // just something that probably exists.
		if (element) {
			return sendChat(value, element.dataset.UUID);
		}
	} else if (action == 22 || action == "sendDirectorChat") {
		var element = getGuestTarget("solo-video", target); // just something that probably exists.
		if (element) {
			return sendChat(value, element.dataset.UUID, true);
		}
	} else if (action == "sendPinnedDirectorChat") {
		var element = getGuestTarget("solo-video", target); // just something that probably exists.
		if (element) {
			return sendChat(value, element.dataset.UUID, 2);
		}
	} else if (action == 27 || action == "volume") {
		var element = getGuestTarget("volume", target);
		if (element) {
			element.value = parseInt(value) || 0;
			return remoteVolume(element);
		}
	} else if ((action == 28) || (action == "setslot")){
		var element = getGuestTarget("setslot", target);
		if (element) {
			return setSlot(element, value);
		} else {
			return false;
		}
	} else if (action == 29 || action == "mixorder") {
		var element = getGuestTarget("order-down", target); 
		if (element) {
			if (value === true) {
				changeOrder(+1,element.dataset.UUID);
			} else if (value === false) {
				changeOrder(-1,element.dataset.UUID);
			} else {
				changeOrder(value,element.dataset.UUID);
			}
			return true;
		} else {
			return false;
		}
	} else if (action == "requestResolution") { // director's preview or scene preview or s/e; not capture resolution
		var element = getGuestTarget("solo-video", target); // just need to find the guest
		if (element) {
			let resolution = value.split("x");
			if (resolution.length==2){
				session.requestResolution(element.dataset.UUID, parseInt(resolution[0]), parseInt(resolution[1]));
				return true;
			} else {
				return "Failed. Must be WIDTHxHEIGHT";
			}
			
		}
		return false;
	} else if (action == "setWidth") { // actual capture resolution ; director only
		var element = getGuestTarget("solo-video", target); // just need to find the guest
		if (element) {
			requestVideoHack("width", parseInt(value), element.dataset.UUID);
			return true;
		}
		return false;
	} else if (action == "setHeight") {
		var element = getGuestTarget("solo-video", target); // just need to find the guest
		if (element) {
			requestVideoHack("height", parseInt(value), element.dataset.UUID);
			return true;
		}
		return false;
	} else if (action == "setAspectRatio") {
		var element = getGuestTarget("solo-video", target); // just need to find the guest
		if (element) {
			requestVideoHack("aspectRatio", parseFloat(value), element.dataset.UUID);
			return true;
		}
		return false;
	} else if (action == "requestAspectRatio") {
		var element = getGuestTarget("solo-video", target); // just need to find the guest
		if (element) {
			let maxDimension = parseInt(value2) || 1920;
			let aspectRatio = 16/9; // default
			
			// Parse aspect ratio
			if (value) {
				if (value.includes(":")) {
					let parts = value.split(":");
					aspectRatio = parseFloat(parts[0]) / parseFloat(parts[1]);
				} else {
					aspectRatio = parseFloat(value);
				}
			}
			
			// Calculate dimensions
			let width, height;
			if (aspectRatio >= 1) {
				width = maxDimension;
				height = Math.round(maxDimension / aspectRatio);
			} else {
				height = maxDimension;
				width = Math.round(maxDimension * aspectRatio);
			}
			
			session.requestResolution(element.dataset.UUID, width, height);
			return true;
		}
		return false;
	} else if (action == "startRoomTimer") {
		var element = getGuestTarget("create-timer", target);
		if (element) {
			element.value = 0;
			return directTimer(element, false, value);
		}
	} else if (action == "pauseRoomTimer") {
		var element = getGuestTarget("create-timer", target);
		if (element) {
			if (element.value == 3) {
				return directTimer(element, { ctrlKey: true });
			} else {
				return directTimer(element, { ctrlKey: true });
			}
		}
	} else if (action == "stopRoomTimer") {
		var element = getGuestTarget("create-timer", target);
		if (element) {
			element.value = 1;
			return directTimer(element);
		}
	} else if (Commands[action]) {
		try {
			return Commands[action](value, target);
		} catch (e) {
			errorlog(e);
		}
	}
	return false;
}

function oscClient() {
	// api.vdo.ninja api OSC (websocket / https API hotkey support).  The iFrame API method provides greater customization.
	if (!session.api) {
		return;
	}
	warnlog("oscClient started");

	var socket = null;
	var connecting = false;
	var failedCount = 0;

	function connect() {
		clearTimeout(connecting);
		if (socket) {
			if (socket.readyState === socket.OPEN) {
				return;
			}
			try {
				socket.close();
			} catch (e) {}
		}
		socket = new WebSocket(session.apiserver);

		socket.onclose = function () {
			session.apiSocket = false;
			failedCount += 1;
			clearTimeout(connecting);
			connecting = setTimeout(function () {
				connect();
			}, 100 * (failedCount - 1));
		};

		socket.onerror = function () {
			failedCount += 1;
			clearTimeout(connecting);
			connecting = setTimeout(function () {
				connect();
			}, 100 * failedCount);
		};

		socket.onopen = function () {
			failedCount = 0;
			try {
				socket.send(JSON.stringify({ join: session.api }));
				session.apiSocket = socket;
				if (queuedSendingAPIMsgs.length) {
					queuedSendingAPIMsgs.forEach(msg => {
						pokeAPI(msg[0], msg[1], msg[2]);
					});
					queuedSendingAPIMsgs = [];
				}
				pokeAPI("details", getDetailedState(session.streamID));
			} catch (e) {
				connecting = setTimeout(function () {
					connect();
				}, 1);
			}
		};

		socket.addEventListener("message", async function (event) {
			if (event.data) {
				var data = JSON.parse(event.data);

				if ("msg" in data) {
					data = data.msg;
				}

				if ("value" in data) {
					if ("action" in data && data.action == "layout") {
						data.value = safelyDecodeValue(data.value);
					}
				}

				var resp = await processMessage(data);
				if (resp !== null) {
					var ret = {};
					data.result = resp;
					ret.callback = data;
					log(ret);
					socket.send(JSON.stringify(ret));
				}
			}
		});
	}
	connect();
}

function safelyDecodeValue(value) { // since the layout can be a number, json, or base64 encoded json
    if (Number.isInteger(Number(value))) {
        return parseInt(value, 10);
    }
    try {
        return JSON.parse(value);  // Try parsing as JSON
    } catch (e) {
    }
    try {
        const decodedValue = atob(value); // Try decoding as base64 instead
        try {
            return JSON.parse(decodedValue); // Try parsing as JSON now that we did b64 decoding
        } catch (e) {
            return decodedValue;
        }
    } catch (e) {
        return value;
    }
}

function setupCommands() {
	var commands = {};

	commands.raisehand = function (value = null, value2 = null) {
		return raisehand();
	};
	commands.togglehand = function (value = null, value2 = null) {
		return raisehand();
	};
	commands.togglescreenshare = function (value = null, value2 = null) {
		toggleScreenShare();
		return session.screenShareState;
	};
	commands.chat = function (value = null, value2 = null) {
		toggleChat(value);
		return session.chat;
	};
	commands.speaker = function (value = null, value2 = null) {
		if (value === true) {
			// unmute
			session.speakerMuted = false; // set
			toggleSpeakerMute(true); // apply
		} else if (value === false) {
			// mute
			session.speakerMuted = true; // set
			toggleSpeakerMute(true); // apply
		} else if (value === "toggle") {
			// toggle
			toggleSpeakerMute();
		}
		return session.speakerMuted;
	}; // mute speaker
	commands.mic = function (value = null, value2 = null) {
		if (value === true) {
			// unmute
			session.muted = false; // set
			log(session.muted);
			toggleMute(true); // apply
		} else if (value === false) {
			// mute
			session.muted = true; // set
			log(session.muted);
			toggleMute(true); // apply
		} else if (value === "toggle") {
			// toggle
			toggleMute();
		}
		return session.muted;
	};
	commands.camera = function (value = null, value2 = null) {
		if (value === true) {
			// unmute
			session.videoMuted = false; // set
			log(session.videoMuted);
			toggleVideoMute(true); // apply
		} else if (value === false) {
			// mute
			session.videoMuted = true; // set
			log(session.videoMuted);
			toggleVideoMute(true); // apply
		} else if (value === "toggle") {
			// toggle
			toggleVideoMute();
		}
		return session.videoMuted;
	};
	commands.video = function (value = null, value2 = null) {
		if (value === true) {
			// unmute
			session.videoMuted = false; // set
			log(session.videoMuted);
			toggleVideoMute(true); // apply
		} else if (value === false) {
			// mute
			session.videoMuted = true; // set
			log(session.videoMuted);
			toggleVideoMute(true); // apply
		} else if (value === "toggle") {
			// toggle
			toggleVideoMute();
		}
		return session.videoMuted;
	};
	commands.hangup = function (value = null, value2 = null) {
		hangup();
		return true;
	};
	commands.bitrate = function (value = null, value2 = null) {
		if (value === false) {
			value = 0;
		} else if (value === true) {
			value = -1;
		} else {
			value = parseInt(value) || 0;
		}
		for (var i in session.rpcs) {
			try {
				session.requestRateLimit(value, i);
			} catch (e) {
				errorlog(e);
			}
		}
		return value;
	};

	commands.getDetails = function (value = null, value2 = null) {
		return getDetailedState();
	};

	commands.getStats = function (value = null, value2 = null) {
		return getQuickStats(value);
	};

	commands.getGuestList = function (value = null, value2 = null) {
		return getGuestList();
	};

	commands.reload = function (value = null, value2 = null) {
		reloadRequested();
		return true;
	};
	commands.volume = function (value = null, value2 = null) {
		if (value === false) {
			value = 0;
		} else if (value === true) {
			value = 100;
		} else {
			value = parseInt(value) || 0;
		}
		value = parseFloat(value / 100);
		for (var i in session.rpcs) {
			try {
				session.rpcs[i].videoElement.volume = parseFloat(value);
			} catch (e) {
				errorlog(e);
			}
		}
		return value;
	};

	commands.forceKeyframe = function (value = null, value2 = null) {
		return session.forcePLI();
	};

	commands.panning = function (value = null, value2 = null) {
		if (value === false) {
			value = 90;
		} else if (value === true) {
			value = 90;
		} else {
			value = parseInt(value);
		}
		for (var uuid in session.rpcs) {
			try {
				adjustPan(uuid, value); // &panning needs to be added to enable. playback only; not mic out.
			} catch (e) {
				errorlog(e);
			}
		}
		return value;
	};

	commands.record = function (value = null, value2 = null) {
		if (!session.videoElement) {
			return;
		}

		if (value === false) {
			// mute
			if ("recording" in session.videoElement) {
				recordLocalVideo("stop");
			}
		} else if (value === true) {
			if ("recording" in session.videoElement) {
				// already recording
			} else {
				recordLocalVideo("start");
			}
		}
		return value;
	};

	commands.group = function (value = null, value2 = null) {
		if (value && value !== "null") {
			return changeGroupDirectorAPI(value);
		}
		return false;
	};

	commands.joinGroup = function (value = null, value2 = null) {
		if (value && value !== "null") {
			return changeGroupDirectorAPI(value, true);
		}
		return false;
	};

	commands.leaveGroup = function (value = null, value2 = null) {
		if (value && value !== "null") {
			return changeGroupDirectorAPI(value, false);
		}
		return false;
	};

	commands.viewGroup = function (value = null, value2 = null) {
		if (value && value !== "null") {
			return changeGroupViewDirectorAPI(value);
		}
		return false;
	};

	commands.joinViewGroup = function (value = null, value2 = null) {
		if (value && value !== "null") {
			return changeGroupViewDirectorAPI(value, true);
		}
		return false;
	};

	commands.leaveViewGroup = function (value = null, value2 = null) {
		if (value && value !== "null") {
			return changeGroupViewDirectorAPI(value, false);
		}
		return false;
	};

	commands.sendChat = function (value = null, value2 = null) {
		sendChat(value);
		// sendChatMessage // this would add it to the chat message
		return true;
	};

	commands.sendChatMessage = function (value = null, value2 = null) {
		sendChatMessage(value);
		return true;
	};
	
	commands.showChatOverlay = function (value = null, value2 = null) {
		getChatMessage(value, false, false, true);
		return true;
	};

	commands.startRoomTimer = function (value = null, value2 = null) {
		getById("globalTimerDirectorToggle").value = 0; // reset
		directRoomTimer(getById("globalTimerDirectorToggle"), false, value);
		return true;
	};

	commands.pauseRoomTimer = function (value = null, value2 = null) {
		if (getById("globalTimerDirectorToggle").value == 3) {
			directRoomTimer(getById("globalTimerDirectorToggle"), { ctrlKey: true }, value);
		} else {
			directRoomTimer(getById("globalTimerDirectorToggle"), { ctrlKey: true }, value);
		}
		return true;
	};

	commands.stopRoomTimer = function (value = null, value2 = null) {
		getById("globalTimerDirectorToggle").value = 1; // pause
		directRoomTimer(getById("globalTimerDirectorToggle"), false, value);
		return true;
	};

	commands.tallylight = function (value = null, value2 = null) {
		if (value == "onair") {
			session.tallyOverride = 1;
		} else if (value == "active") {
			session.tallyOverride = 2;
		} else if (value == "standby") {
			session.tallyOverride = 3;
		} else if (value == "false") {
			session.tallyOverride = false;
		} else if (value == "off") {
			session.tallyOverride = false;
		} else if (value) {
			session.tallyOverride = parseInt(value) || 0;
		} else {
			session.tallyOverride = 0;
		}
		applySceneState();
		return true;
	};

	commands.prevSlide = function (value = null, value2 = null) {
		var data = {};
		data.d = [176, 110, 10];
		playbackMIDI(data);
		return true;
	};

	commands.nextSlide = function (value = null, value2 = null) {
		var data = {};
		data.d = [176, 110, 11];
		playbackMIDI(data);
		return true;
	};

	commands.zoom = function (value = null, value2 = null) {
		if (value !== null) {
			const zoomValue = parseFloat(value);
			const isAbsolute = value2 === true || value2 === "true" || value2 === "abs";
			session.remoteZoom(zoomValue, isAbsolute);
			return { zoom: zoomValue, absolute: isAbsolute };
		}
		return false;
	};
	
	commands.focus = function (value = null, value2 = null) {
		if (value !== null) {
			const focusValue = parseFloat(value);
			const isAbsolute = value2 === true || value2 === "true" || value2 === "abs";
			session.remoteFocus(focusValue, isAbsolute);
			return { focus: focusValue, absolute: isAbsolute };
		}
		return false;
	};
	
	commands.pan = function (value = null, value2 = null) {
		if (value !== null) {
			const panValue = parseFloat(value);
			const isAbsolute = value2 === true || value2 === "true" || value2 === "abs";
			session.remotePan(panValue, isAbsolute);
			return { pan: panValue, absolute: isAbsolute };
		}
		return false;
	};
	
	commands.tilt = function (value = null, value2 = null) {
		if (value !== null) {
			const tiltValue = parseFloat(value);
			const isAbsolute = value2 === true || value2 === "true" || value2 === "abs";
			session.remoteTilt(tiltValue, isAbsolute);
			return { tilt: tiltValue, absolute: isAbsolute };
		}
		return false;
	};
	
	commands.exposure = function (value = null, value2 = null) {
		if (value !== null) {
			const exposureValue = parseFloat(value);
			const isAbsolute = value2 === true || value2 === "true" || value2 === "abs";
			session.remoteExposure(exposureValue, isAbsolute);
			return { exposure: exposureValue, absolute: isAbsolute };
		}
		return false;
	};
	
	commands.soloVideo = function (value = null, value2 = null) {
		var element = getById("highlightDirector");
		if (value && value == "toggle") {
			return requestInfocus(element);
		} else if (value && value !== "null") {
			return requestInfocus(element, null, true);
		} else if (value && value === "null") {
			return requestInfocus(element);
		} else {
			return requestInfocus(element, null, false);
		}
		return false;
	};
	commands.highlight = function (value = null, value2 = null) {
		return commands.soloVideo(value, value2);
	};
	
	commands.activeSpeaker = function (value = null, value2 = null) {
		var res = {};
		res.previous = session.activeSpeaker;
		if ((value && (value == "toggle" || value=="null")) || (value===null)) {
			res.action = "toggle";
			if (session.activeSpeaker){
				session.activeSpeaker = false;
			} else {
				session.activeSpeaker = urlParams.get("activespeaker") || urlParams.get("speakerview") || urlParams.get("sas") || 1;
			}
		} else if (value && parseInt(value)){
			session.activeSpeaker = parseInt(value) || 1;
		} else {
			session.activeSpeaker = false;
		}
		if (session.activeSpeaker && !session.activeSpeakerInterval){
			if (!session.audioEffects){
				session.audioEffects = true;
				for (var UUID in session.rpcs) {
					updateIncomingAudioElement(UUID);
				}
			}
			activeSpeaker(false);
			session.activeSpeakerInterval = setInterval(function () {
				activeSpeaker(false);
			}, 100);
		} else {
			updateMixer();
		}
		res.current = session.activeSpeaker;
		res.value = value;
		return res;
	};

	commands.setBufferDelay = function (value = null, value2 = null) {
		let delay = parseInt(value) || 0;

		if (!value2) {
			session.buffer = delay;
		} else if (value2 === "*") {
			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;
				});
				return true;
			}
		} else if (value2 in session.rpcs) {
			session.rpcs[value2].buffer = delay;
			playoutdelay(value2);
			document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + value2 + '"] input[data-buffer-value]').forEach(ele => {
				ele.value = delay;
			});
			return true;
		} else if (value2) {
			let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === value2);
			if (session.rpcs[UUID]) {
				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;
				});
				return true;
			} else {
				errorlog("The stream ID specified does not exist: " + value2);
			}
		}
		return false;
	};

	commands.layout = function (value = null, value2 = null) {
		let response = {};
		response.input = value;
		
		try {
			if (parseInt(value) == value) {
				log(value);
				value = parseInt(value);
				if (value == 0) {
					value = false; // Auto mixer
				} else {
					value -= 1; // Adjust index to match layouts array (1-based to 0-based)
				}
				response.index = value;
			} else if (checkType(value) === "Array") {
				log(value);
				session.layout_array = value;
				if (session.layout_array){
					session.layout = combinedLayout(session.layout_array);
				}
				updateMixer();
				
				if (session.director) {
				   issueLayout("0");
				   response.issued = true;
				   response.scene = "0";
				   pokeIframeAPI("layout-updated", value);
				}
				
				response.type = 1;
				updateMixer();
				response.combined_layout = session.layout;
				var temp = previousDebug;
				previousDebug = response
				return {response:response, previous:temp}
			} else if (checkType(value) === "Object") {
				log(value);
				session.layout = value;
				if (session.director) {
					issueLayout("0");
					response.issued = true;
					response.scene =  "0";
					pokeIframeAPI("layout-updated", session.layout);
				}
				response.type = 2;
				updateMixer();
				response.layout = session.layout;
				var temp = previousDebug;
				previousDebug = response
				return {response:response, previous:temp}
			}

			if (value === null) {
				session.layout = false;
				pokeIframeAPI("layout-updated", session.layout);
				pokeIframeAPI("layout-index", 0);
				if (session.director) {
					issueLayout("0");
					response.issued = true;
					response.scene =  "0";
					
				}
				response.type = 3;
				response.layout = session.layout;
				updateMixer();
				var temp = previousDebug;
				previousDebug = response
				return {response:response, previous:temp}
			} else if (value === false) {
				session.layout = false;
				pokeIframeAPI("layout-updated", session.layout);
				pokeIframeAPI("layout-index", 0);
				if (session.director) {
					issueLayout("0");
					response.issued = true;
					response.scene =  "0";
				}
				response.layout = session.layout;
				response.type = 3;
				updateMixer();
				var temp = previousDebug;
				previousDebug = response
				return {response:response, previous:temp}
			} else if (session.layouts && session.layouts[value]) {
				try {
					log(session.layouts);
					var temp = session.layouts[value];
					response.type = 4;
					if (session.director) {
						var combined = {};
						for (var i = 0; i < temp.length; i++) {
							if (!temp[i]) continue;
							
							let streamID = null;
							
							// First check if there's a slot assigned
							if ("slot" in temp[i]) {
								const slotNumber = parseInt(temp[i].slot) + 1;
								streamID = session.currentSlots[slotNumber];
							}
							
							// If no stream found via slot, check defaultStreamID
							if (!streamID && temp[i].defaultStreamID) {
								// Check if this defaultStreamID is connected and not assigned to another slot
								let isConnected = false;
								let isAlreadyAssigned = false;
								
								for (let j in session.rpcs) {
									if (session.rpcs[j].streamID === temp[i].defaultStreamID) {
										isConnected = true;
										// Check if this stream is assigned to any slot
										for (let slot in session.currentSlots) {
											if (session.currentSlots[slot] === temp[i].defaultStreamID) {
												isAlreadyAssigned = true;
												break;
											}
										}
										break;
									}
								}
								
								if (isConnected && !isAlreadyAssigned) {
									streamID = temp[i].defaultStreamID;
								}
							}
							
							// If we found a streamID, use it, otherwise add to empty slot
							if (streamID) {
								combined[streamID] = temp[i];
							} else {
								if (!combined[""]) combined[""] = [];
								combined[""].push(temp[i]);
							}
						}
						session.layout = combined;
						log("issuing layout:");
						log(session.layout);
						issueLayout("0");
						response.issued = true;
						response.scene =  "0";
						response.combined_layout = session.layout;
						
						pokeIframeAPI("layout-updated", session.layout);
						pokeIframeAPI("layout-index", value + 1);
					}
				} catch (e) {
					errorlog(e);
					response.error = e.message
				}
				updateMixer();
				temp = previousDebug;
				previousDebug = response
				return {response:response, previous:temp}
			} else {
				errorlog("no layout found");
				log(session.layouts);
				var temp = previousDebug;
				previousDebug = response
				return {response:response, previous:temp}
			}
		} catch (e) {
			response.error = e.message
			errorlog(e);
		}
		var temp = previousDebug;
		previousDebug = response
		return {response:response, previous:temp}
	};
	
	commands.width = function (value = null, value2 = null) {
		// affects LOCAL camera width
		let width = value ? parseInt(value) : null;
		if (width) {
			updateCameraConstraints("width", width, false, false);
			return true;
		}
		return false;
	};

	commands.height = function (value = null, value2 = null) {
		// affects LOCAL camera height
		let height = value ? parseInt(value) : null;
		if (height) {
			updateCameraConstraints("height", height, false, false);
			return true;
		}
		return false;
	};

	commands.aspectRatio = function (value = null, value2 = null) {
		// affects LOCAL camera aspect ratio
		if (!value) return false;
		
		let aspectRatio;
		if (typeof value === 'string' && value.includes(":")) {
			let parts = value.split(":");
			aspectRatio = parseFloat(parts[0]) / parseFloat(parts[1]);
		} else {
			aspectRatio = parseFloat(value);
		}
		
		if (aspectRatio && !isNaN(aspectRatio)) {
			updateCameraConstraints("aspectRatio", aspectRatio, false, false);
			return true;
		}
		return false;
	};

	commands.videoConstraint = function (value = null, value2 = null) {
		// Generic video constraint setter for LOCAL camera
		// Usage: action=videoConstraint&value=CONSTRAINT_NAME&value2=CONSTRAINT_VALUE
		if (!value || value2 === null || value2 === undefined) return false;
		
		// Parse value2 based on common types
		let constraintValue = value2;
		
		// Handle boolean strings
		if (value2 === "true") {
			constraintValue = true;
		} else if (value2 === "false") {
			constraintValue = false;
		} else if (value2 == parseFloat(value2)) {
			// Handle numeric values
			constraintValue = parseFloat(value2);
		}
		
		// Special handling for aspectRatio with colon notation
		if (value === "aspectRatio" && typeof value2 === 'string' && value2.includes(":")) {
			let parts = value2.split(":");
			constraintValue = parseFloat(parts[0]) / parseFloat(parts[1]);
		}
		
		// Apply the constraint
		updateCameraConstraints(value, constraintValue, false, false);
		return true;
	};

	return commands;
}
var Commands = setupCommands();
var previousDebug = {};

function checkType(value) {
  if (Array.isArray(value)) {
    return 'Array';
  } else if (typeof value === 'object' && value !== null) {
    return 'Object';
  } else {
    return 'Neither an Array nor an Object';
  }
}
async function processMessage(data) {
	// api.vdo.ninja/apikey/action/value
	try {
		warnlog(data);
		if ("target" in data && data.target !== "null" && data.target !== null) {
			if ("action" in data) {
				if ("value" in data) {
					return await targetGuest(data.target, data.action, data.value, data.value2 || null);
				} else {
					return await targetGuest(data.target, data.action, null);
				}
			}
		} else if ("action" in data) {
			if (data.action in Commands) {
				if ("value" in data) {
					if (data.value == "true") {
						data.value = true;
					} else if (data.value == "false") {
						data.value = false;
					}
					return Commands[data.action](data.value, data.value2 || null);
				} else {
					return Commands[data.action]();
				}
			}
		}
	} catch (e) {
		errorlog(e);
	}
	return null;
}

function midiHotkeysNote(note, velocity = false) {
	if (session.midiHotkeys == 1) {
		if (note == "G3") {
			// open and close the chat window
			toggleChat();
			return session.chat;
		} else if (note == "A3") {
			// mute your audio output
			toggleMute();
			return session.muted;
		} else if (note == "B3") {
			// mute your video output
			toggleVideoMute();
			return session.videoMuted;
		} else if (note == "C4") {
			// enable / disable screenshare
			toggleScreenShare();
			return session.screenShareState;
		} else if (note == "D4") {
			// completely kill your connection/session
			hangup();
			return true;
		} else if (note == "E4") {
			// raise your hand; director sees this
			raisehand();
			return raisehand();
		} else if (note == "F4") {
			// start/stop local recording
			return recordLocalVideoToggle();
		} else if (note == "G4") {
			// Director Enables their Audio output
			press2talk(true);
			return true;
		} else if (note == "A4") {
			// Director cut's their audio/video output
			hangup2();
			return true;
		} else if (note == "B4") {
			// toggle speaker
			toggleSpeakerMute();
			return session.speakerMuted;
		}
	} else if (session.midiHotkeys == 2) {
		if (note == "G1") {
			// open and close the chat window
			toggleChat();
		} else if (note == "A1") {
			// mute your audio output
			toggleMute();
		} else if (note == "B1") {
			// mute your video output
			toggleVideoMute();
		} else if (note == "C2") {
			// enable / disable screenshare
			toggleScreenShare();
		} else if (note == "D2") {
			// completely kill your connection/session
			hangup();
		} else if (note == "E2") {
			// raise your hand; director sees this
			raisehand();
		} else if (note == "F2") {
			// start/stop local recording
			recordLocalVideoToggle();
		} else if (note == "G2") {
			// Director Enables their Audio output
			press2talk(true);
		} else if (note == "A2") {
			// Director cut's their audio/video output
			hangup2();
		} else if (note == "B2") {
			//  toggle speaker
			toggleSpeakerMute();
		}
	} else if (session.midiHotkeys == 3) {
		if (note == "C1") {
			if (velocity == "0") {
				// open and close the chat window
				toggleChat();
			} else if (velocity == "1") {
				// mute your audio output
				toggleMute();
			} else if (velocity == "2") {
				// mute your video output
				toggleVideoMute();
			} else if (velocity == "3") {
				// enable / disable screenshare
				toggleScreenShare();
			} else if (velocity == "4") {
				// completely kill your connection/session
				hangup();
			} else if (velocity == "5") {
				// raise your hand; director sees this
				raisehand();
			} else if (velocity == "6") {
				// start/stop local recording
				recordLocalVideoToggle();
			} else if (velocity == "7") {
				// Director Enables their Audio output
				press2talk(true);
			} else if (velocity == "8") {
				// Director cut's their audio/video output
				hangup2();
			} else if (velocity == "9") {
				// toggle speaker
				toggleSpeakerMute();
			}
		}
	}
/* 	if (velocity !== false && typeof velocity !== "undefined") {
		// Get integer value of velocity
		const velocityValue = parseInt(velocity);
		
		// Check if valid MIDI velocity (0-127)
		if (!isNaN(velocityValue) && velocityValue >= 0 && velocityValue <= 127) {
			// Camera control MIDI commands using Channel 1, various CC numbers
			if (note == "C5") {
				// Zoom - scale 0-127 to percentage or use relative value
				const normalizedValue = velocityValue / 127; // 0 to 1 range
				session.remoteZoom(normalizedValue, true); // absolute value
				return { zoom: normalizedValue, absolute: true };
			} else if (note == "D5") {
				// Focus - scale 0-127 to focus value
				const normalizedValue = velocityValue / 127; // 0 to 1 range
				session.remoteFocus(normalizedValue);
				return { focus: normalizedValue };
			} else if (note == "E5") {
				// Pan - scale 0-127 to pan value
				const normalizedValue = (velocityValue - 64) / 64; // -1 to 1 range
				session.remotePan(normalizedValue);
				return { pan: normalizedValue };
			} else if (note == "F5") {
				// Tilt - scale 0-127 to tilt value
				const normalizedValue = (velocityValue - 64) / 64; // -1 to 1 range
				session.remoteTilt(normalizedValue);
				return { tilt: normalizedValue };
			} else if (note == "G5") {
				// Exposure - scale 0-127 to exposure value
				const normalizedValue = velocityValue / 127; // 0 to 1 range
				session.remoteExposure(normalizedValue);
				return { exposure: normalizedValue };
			}
		}
	} */
}

function getRightOrderedElement(selector, guestslot, UUID = false) {
	var elements = getById("guestFeeds").children;
	if (!UUID) {
		for (var i = 0; i < elements.length; i++) {
			try {
				UUID = elements[i].UUID;
				var lock = parseInt(document.getElementById("position_" + UUID).dataset.locked);
				if (lock && lock == guestslot + 1) {
					return elements[i].querySelector(selector) || false;
				}
			} catch (e) {}
		}
	}

	if (elements[guestslot]) {
		return elements[guestslot].querySelector(selector) || false;
	} else {
		return false;
	}
}

function midiHotkeysCommand_offset(command, value, offset = 1) {
	for (var i = 0; i < 9; i++) {
		if (command == offset + i) {
			var ele = getRightOrderedElement('[data-action-type="mute-guest"][data--u-u-i-d]', command - offset);
			if (ele) {
				remoteMute(ele, true);
			}
		}
	}
}

function midiHotkeysCommand(command, value) {
	if (command == 110) {
		// Existing controls 0-8, 10-11 remain unchanged
		if (value == 0) {
			toggleChat();
		} else if (value == 1) {
			toggleMute();
		} else if (value == 2) {
			toggleVideoMute();
		} else if (value == 3) {
			toggleScreenShare();
		} else if (value == 4) {
			hangup();
		} else if (value == 5) {
			raisehand();
		} else if (value == 6) {
			recordLocalVideoToggle();
		} else if (value == 7) {
			press2talk(true);
		} else if (value == 8) {
			hangup2();
		}
		// 10 & 11 reserved for PPT slides
		
		// Camera controls - relative adjustments
		else if (value == 20) {
			// Zoom in (relative +10%)
			Commands.zoom(0.1);
		} else if (value == 21) {
			// Zoom out (relative -10%)
			Commands.zoom(-0.1);
		} else if (value == 22) {
			// Pan left (relative -10%)
			Commands.pan(-0.1);
		} else if (value == 23) {
			// Pan right (relative +10%)
			Commands.pan(0.1);
		} else if (value == 24) {
			// Tilt up (relative +10%)
			Commands.tilt(0.1);
		} else if (value == 25) {
			// Tilt down (relative -10%)
			Commands.tilt(-0.1);
		} else if (value == 26) {
			// Exposure increase (relative +10%)
			Commands.exposure(0.1);
		} else if (value == 27) {
			// Exposure decrease (relative -10%)
			Commands.exposure(-0.1);
		} else if (value == 28) {
			// Focus near (relative -10%)
			Commands.focus(-0.1);
		} else if (value == 29) {
			// Focus far (relative +10%)
			Commands.focus(0.1);
		}
		
		// Camera presets - absolute positions
		else if (value == 30) {
			// Camera preset 1: Center position
			Commands.zoom(1.0, "abs");
			Commands.pan(0, "abs");
			Commands.tilt(0, "abs");
		} else if (value == 31) {
			// Camera preset 2: Wide shot
			Commands.zoom(0.5, "abs");
			Commands.pan(0, "abs");
			Commands.tilt(0, "abs");
		} else if (value == 32) {
			// Camera preset 3: Close-up
			Commands.zoom(2.0, "abs");
			Commands.pan(0, "abs");
			Commands.tilt(0, "abs");
		}
		
	} else if (command > 110) {
		// Existing guest slot controls remain unchanged
		var guestslot = command - 111;
		if (value == 0) {
			var ele = getRightOrderedElement('[data-action-type="forward"][data--u-u-i-d]', guestslot);
			if (ele) {
				directMigrate(ele, true);
			}
		} else if (value == 1) {
			var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="1"][data--u-u-i-d]', guestslot);
			if (ele) {
				directEnable(ele, true);
			}
		} else if (value == 2) {
			var ele = getRightOrderedElement('[data-action-type="mute-scene"][data--u-u-i-d]', guestslot);
			if (ele) {
				directMute(ele, true);
			}
		} else if (value == 3) {
			var ele = getRightOrderedElement('[data-action-type="mute-guest"][data--u-u-i-d]', guestslot);
			if (ele) {
				remoteMute(ele, true);
			}
		} else if (value == 4) {
			var ele = getRightOrderedElement('[data-action-type="hangup"][data--u-u-i-d]', guestslot);
			if (ele) {
				directHangup(ele, true);
			}
		} else if (value == 5) {
			var ele = getRightOrderedElement('[data-action-type="solo-chat"][data--u-u-i-d]', guestslot);
			if (ele) {
				session.toggleSoloChat(ele.dataset.UUID);
			}
		} else if (value == 6) {
			var ele = getRightOrderedElement('[data-action-type="toggle-remote-speaker"][data--u-u-i-d]', guestslot);
			if (ele) {
				remoteSpeakerMute(ele);
			}
		} else if (value == 7) {
			var ele = getRightOrderedElement('[data-action-type="toggle-remote-display"][data--u-u-i-d]', guestslot);
			if (ele) {
				remoteDisplayMute(ele);
			}
		} else if (value == 8) {
			var ele = getRightOrderedElement('[data-action-type="force-keyframe"][data--u-u-i-d]', guestslot);
			if (ele) {
				requestKeyframeScene(ele);
			}
		} else if (value == 12) {
			var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="2"][data--u-u-i-d]', guestslot);
			if (ele) {
				directEnable(ele, true);
			}
		} else if (value == 13) {
			var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="3"][data--u-u-i-d]', guestslot);
			if (ele) {
				directEnable(ele, true);
			}
		} else if (value == 14) {
			var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="4"][data--u-u-i-d]', guestslot);
			if (ele) {
				directEnable(ele, true);
			}
		} else if (value == 15) {
			var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="5"][data--u-u-i-d]', guestslot);
			if (ele) {
				directEnable(ele, true);
			}
		} else if (value == 16) {
			var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="6"][data--u-u-i-d]', guestslot);
			if (ele) {
				directEnable(ele, true);
			}
		} else if (value == 17) {
			var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="7"][data--u-u-i-d]', guestslot);
			if (ele) {
				directEnable(ele, true);
			}
		} else if (value == 18) {
			var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="8"][data--u-u-i-d]', guestslot);
			if (ele) {
				directEnable(ele, true);
			}
		} else if (value >= 27) {
			var ele = getRightOrderedElement('[data-action-type="volume"][data--u-u-i-d]', guestslot);
			if (ele) {
				var audioGain = parseInt(value - 27) || 0;
				if (audioGain < 0) {
					audioGain = 0;
				}

				ele.value = 200;
				for (var i = 1; i <= 200; i++) {
					if (volumeLUT[i] >= audioGain) {
						ele.value = i;
						break;
					}
				}
				remoteVolume(ele);
			}
		}
	}
	
	// Additional MIDI CC commands for finer camera control (80-89)
	else if (command >= 80 && command <= 89) {
		// Map MIDI CC values (0-127) to camera control values
		const normalizedValue = value / 127; // 0 to 1 range
		
		if (command == 80) {
			// CC80: Zoom absolute (0-127 maps to 0-2x zoom)
			Commands.zoom(normalizedValue * 2, "abs");
		} else if (command == 81) {
			// CC81: Pan absolute (0-127 maps to -1 to +1)
			Commands.pan((normalizedValue * 2) - 1, "abs");
		} else if (command == 82) {
			// CC82: Tilt absolute (0-127 maps to -1 to +1)
			Commands.tilt((normalizedValue * 2) - 1, "abs");
		} else if (command == 83) {
			// CC83: Exposure absolute (0-127 maps to 0-1)
			Commands.exposure(normalizedValue, "abs");
		} else if (command == 84) {
			// CC84: Focus absolute (0-127 maps to 0-1)
			Commands.focus(normalizedValue, "abs");
		}
	}
}

session.createResourceChannel = function(UUID) {
    if (!session.pcs[UUID] || !session.pcs[UUID].allowResources) return;
    
    const channel = session.pcs[UUID].createDataChannel("resources", {
        ordered: true,
        maxRetransmits: 30
    });
    
    session.pcs[UUID].resourceChannel = channel;
    if (session.pcs[UUID] && !session.pcs[UUID].resourceQueue) { // Initialize if not exists
        session.pcs[UUID].resourceQueue = [...session.resources];
    }
    session.pcs[UUID].processingResource = false;
    
    session.pcs[UUID].resourceChannel.onopen = () => {
        log("Resource channel opened");
        if (session.pcs[UUID].resourceQueue && session.pcs[UUID].resourceQueue.length > 0) {
            session.processResourceQueue(UUID); // Process any queued items
        }
    };
    
    session.pcs[UUID].resourceChannel.onclose = () => {
        warnlog("Resource channel closed");
		if (session.pcs[UUID]){
			session.pcs[UUID].processingResource = false; // Reset processing state
		}
    };
    
    session.pcs[UUID].resourceChannel.onerror = (e) => {
		warnlog("Resource channel error");
		if (session.pcs[UUID]){
			session.pcs[UUID].processingResource = false; // Reset processing state on error
		}
    };
};
async function handleImageUpload(file, field) {
    const buffer = await file.arrayBuffer();
    const resource = {
        type: field.templateName, // Changed from field.type to field.templateName
        id: field.id,
        data: buffer,
        metadata: {
            filename: file.name,
            size: file.size,
			type: field.type,
			label: field.type,
            filetype: file.type,
			id: field.id,
            templateName: field.templateName 
        }
    };
    
    for (var UUID in session.pcs) {
        if (session.pcs[UUID].allowResources) {
            session.sendResource(resource, UUID);
        }
    }
    session.resources.push(resource);
}
session.sendResource = async function(resource, UUID) {
    // Resource format: { type: "avatar"|"overlay"|"qr"|"icon", data: ArrayBuffer, metadata: Object }
    if (!session.pcs[UUID]) return false;
    
    if (!session.pcs[UUID].resourceQueue) {
        session.pcs[UUID].resourceQueue = [...session.resources]; // Initialize queue if not exists
    }
    
    // Queue the resource
    session.pcs[UUID].resourceQueue.push(resource);
    
    // If channel exists and is open, process queue
    if (session.pcs[UUID].resourceChannel && session.pcs[UUID].resourceChannel.readyState === "open") {
        if (!session.pcs[UUID].processingResource) {
            session.processResourceQueue(UUID);
        }
    } else if (!session.pcs[UUID].resourceChannel && session.pcs[UUID].allowResources) {
        // If channel doesn't exist but resources are allowed, create it
        session.createResourceChannel(UUID);
    }
    
    return true;
};

session.processResourceQueue = async function(UUID) {
    if (!session.pcs[UUID] || !session.pcs[UUID].resourceChannel || session.pcs[UUID].processingResource) return;
    
    const channel = session.pcs[UUID].resourceChannel;
    const queue = session.pcs[UUID].resourceQueue;
    
    if (queue.length === 0) return;
    
    session.pcs[UUID].processingResource = true;
    
    try {
        const resource = queue[0];
        const chunkSize = 16384; // 16KB chunks
        const totalChunks = Math.ceil(resource.data.byteLength / chunkSize);
        
        log(resource);
		
        channel.send(JSON.stringify(resource.metadata));
		log("sending..");
        await new Promise(r => setTimeout(r, 100)); // Small delay to ensure metadata is processed
        
        for (let i = 0; i < totalChunks; i++) {
            const start = i * chunkSize;
            const end = Math.min(start + chunkSize, resource.data.byteLength);
            const chunk = resource.data.slice(start, end);
            
            channel.send(chunk);
            
            // Add small delay between chunks to prevent flooding
            await new Promise(r => setTimeout(r, 50));
        }
        log("sent.");
        queue.shift();
        
    } catch (e) {
        errorlog(e);
    }
    
    session.pcs[UUID].processingResource = false;
    
    if (queue.length > 0) {
        setTimeout(() => session.processResourceQueue(UUID), 100);
    }
};

session.recieveResourcesChannel = async function (UUID, channel) {
    if (!session.allowResources){
        warnlog("Someone is trying to send resources despite you asking not to..");
        return;
    }
    log("Created resource channel");
    
    session.rpcs[UUID].resourceChannel = channel;
    session.rpcs[UUID].resourceChannel.binaryType = "arraybuffer";
    
    let metadata = null;
    let receivedChunks = [];
    let receivedSize = 0;
    
    channel.onmessage = async (e) => { // Fixed: Using channel parameter instead of nested property
        try {
            if (typeof e.data === "string") {
                const data = JSON.parse(e.data);
				log(data);
                if (data.templateName) {
                    metadata = data;
                    receivedChunks = [];
                    receivedSize = 0;
                }
            } else {
                if (!metadata) return;
                
                receivedChunks.push(e.data);
                receivedSize += e.data.byteLength;
                
                if (receivedSize >= metadata.size) {
                    const completeBuffer = await new Blob(receivedChunks).arrayBuffer();
                    session.processReceivedResource(UUID, completeBuffer, metadata);
                    
                    receivedChunks = [];
                    receivedSize = 0;
                }
            }
        } catch (error) {
            errorlog(error);
        }
    };
    
    channel.onopen = e => {
        log("Opened resource channel");
    };
};

session.processReceivedResource = function(UUID, buffer, data) {
    if (!session.rpcs[UUID]) return;
    
    const createObjectURL = (buffer, type) => {
        const blob = new Blob([buffer], { type: type || "image/png" });
        return URL.createObjectURL(blob);
    };
    
    try {
        log(data);
		
		session.rpcs[UUID].meta[data.templateName] = data;

        if (session.rpcs[UUID].meta[data.templateName].value) {
            URL.revokeObjectURL(session.rpcs[UUID].meta[data.templateName].value);
        }

        session.rpcs[UUID].meta[data.templateName].value = createObjectURL(buffer, data.type);
        log(session.rpcs[UUID].meta[data.templateName].value);
        updateMixer();
    } catch (error) {
        errorlog(error);
    }
};
function sendRawMIDI(input, UUID = false, streamID = false) {
    var msg = {};
    msg.midi = {};
    msg.midi.d = Array.from(input.data); // Convert to regular array
    msg.midi.s = input.timestamp || Date.now();
    if (input.message && input.message.channel) {
        msg.midi.c = input.message.channel;
    } else if (input && input.channel) {
        msg.midi.c = input.channel;
    }

	if (UUID && session.pcs[UUID] && session.pcs[UUID].allowMIDI) {
		session.sendMessage(msg, UUID);
	} else if (UUID && session.rpcs[UUID] && session.rpcs[UUID].allowMIDI) {
		session.sendRequest(msg, UUID);
	} else if (streamID) {
		for (var UID in session.rpcs) {
			if (session.rpcs[UID].allowMIDI && session.rpcs[UID].streamID === streamID) {
				// specific to gstreamer code aplication
				session.sendRequest(msg, UID);
				return; // only one stream ID should match
			}
		}
	} else {
		var list = [];
		for (var UID in session.pcs) {
			if (session.pcs[UID].allowMIDI) {
				if (session.sendMessage(msg, UID)) {
					list.push(UID);
				}
			}
		}
		for (var UID in session.rpcs) {
			if (session.rpcs[UID].allowMIDI) {
				// specific to gstreamer code aplication
				if (!list.includes(UID)) {
					session.sendRequest(msg, UID);
				}
			}
		}
	}
}

function sendMIDINote(note, on = true, channel = 1, uuid = null) {
    // MIDI Note On status byte: 144 + (channel - 1) 
    // MIDI Note Off status byte: 128 + (channel - 1)
    const statusByte = on ? (144 + (channel - 1)) : (128 + (channel - 1));
    const velocity = on ? 127 : 0; // 127 for note on, 0 for note off
    
    // Convert note names like "C1", "D3" to MIDI note numbers
    let noteNumber;
    if (typeof note === "string") {
        const noteName = note.slice(0, -1);
        const octave = parseInt(note.slice(-1));
        const noteValues = { "C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3, 
                            "E": 4, "F": 5, "F#": 6, "Gb": 6, "G": 7, 
                            "G#": 8, "Ab": 8, "A": 9, "A#": 10, "Bb": 10, "B": 11 };
        
        // C1 is MIDI note 24, each octave is 12 notes
        noteNumber = 24 + (octave - 1) * 12 + noteValues[noteName];
    } else {
        noteNumber = note;
    }
    
    // Create MIDI message and send it
    const data = {};
    data.data = [statusByte, noteNumber, velocity];
    sendRawMIDI(data, uuid);
    
    return { note: noteNumber, status: statusByte, velocity: velocity };
}
function buttonMIDI(ele, state = null) {
    const note = ele.dataset.midiNote;
    const uuid = ele.dataset.uuid || null;
    const isToggleMode = ele.dataset.midiMode === 'toggle';
    
    // Handle state tracking similar to changeGroup function
    let newState;
    let changed = false;
    
    if (state === true) {
        // Explicit true state requested
        if (!ele.classList.contains("pressed") || CtrlPressed) {
            changed = true;
            ele.classList.add("pressed");
            ele.ariaPressed = "true";
        }
        newState = true;
    } else if (state === false) {
        // Explicit false state requested
        if (ele.classList.contains("pressed") || CtrlPressed) {
            changed = true;
            ele.classList.remove("pressed");
            ele.ariaPressed = "false";
        }
        newState = false;
    } else if (CtrlPressed){
        newState = ele.classList.contains("pressed");
        changed = true;
    } else {
        // Toggle current state
        newState = !ele.classList.contains("pressed");
        changed = true;
        
        if (newState) {
            ele.classList.add("pressed");
            ele.ariaPressed = "true";
        } else {
            ele.classList.remove("pressed");
            ele.ariaPressed = "false";
        }
    }
    
    // Only send MIDI if state actually changed
    if (changed) {
        if (isToggleMode) {
            // Dual channel toggle
            sendMIDINote(note, true, newState ? 1 : 2, uuid);
        } else {
            // Single channel note on/off
            const channel = parseInt(ele.dataset.midiChannel || "1");
            sendMIDINote(note, newState, channel, uuid);
            
            // Only auto-unpress non-toggle buttons
            if (newState) {
                setTimeout(() => {
                    ele.classList.remove("pressed");
                    ele.ariaPressed = "false";
                }, 120);
            }
        }
        
        // Sync director state if available
        if (typeof syncDirectorState === 'function') {
            syncDirectorState(ele);
        }
    }
    
    return newState;
}

let currentOscillatorIdMidi = 0;

function setupMidiOscillator(callbackFunction) {
    if (!thisOscillatorId) {
        thisOscillatorId = ++currentOscillatorIdMidi;
    } else if (currentOscillatorIdMidi !== thisOscillatorId) {
        return false;
    }

    if (!session.audioCtx) {
        session.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }

    let oscillator = session.audioCtx.createOscillator();
    let silence = session.audioCtx.createGain();
    silence.gain.value = 0;
    oscillator.connect(silence);
    silence.connect(session.audioCtx.destination);

    if (!timeOne) {
        timeOne = session.audioCtx.currentTime;
    }

    oscillator.onended = () => {
        oscillator.disconnect();
        silence.disconnect();
        if (currentOscillatorIdMidi === thisOscillatorId) {
            let timeTwo = session.audioCtx.currentTime;
            if (typeof callbackFunction === "function") {
                callbackFunction();
            }
            setupMidiOscillator(callbackFunction, frameRate, timeTwo, thisOscillatorId);
        }
    };

    oscillator.start(timeOne);
    oscillator.stop(timeOne + 1 / frameRate);

    return function (check = false) {
        if (check && currentOscillatorIdMidi !== thisOscillatorId) {
            return true;
        } else if (check) {
            return false;
        }
        if (currentOscillatorIdMidi === thisOscillatorId) {
            currentOscillatorIdMidi++;
        }
        return false;
    };
}

function playOutMidi(msg) {
    if (session.midiIn === true || session.midiIn == parseInt(session.midiIn)) {
        if ("d" in msg) {
            const outputs = session.midiIn === true ? WebMidi.outputs : [WebMidi.outputs[parseInt(session.midiIn) - 1]];
            
            outputs.forEach(output => {
                try {
                    if ("c" in msg) {
                        output.channels[msg.c].send(msg.d);
                    } else if (msg.d){
                        output.send(msg.d);
                    }
                } catch (e) {
                    errorlog(e);
                }
            });
        }
    }
}

function displayOverlayMessage(label, msg) {
    if (!(session.cleanOutput && session.cleanish == false)) {
        var textOverlay = getById("overlayMsgs");
        if (textOverlay) {
            textOverlay.innerText = msg
            textOverlay.style.display = "block";
            var showtime = msg.length * 200 + 3000;
            if (showtime > 8000) {
                showtime = 8000;
            }
            setTimeout(
                function (ele) {
                    try {
                        ele.parentNode.removeChild(ele);
                    } catch (e) {}
                },
                showtime,
                spanOverlay
            );
        }
    }
}

let lastMTCValues = { hours: 0, minutes: 0, seconds: 0, frames: 0, type: 0 };
let mtcCounter = 0;
let lastDisplayedTime = 0;
function handleQuarterFrame(data) {
    const mtcType = (data >> 4) & 0x7;
    const mtcValue = data & 0xF;

    //console.log(`Quarter Frame: Type ${mtcType}, Value ${mtcValue}`);

    switch (mtcType) {
        case 0: lastMTCValues.frames = mtcValue; break;
        case 1: lastMTCValues.frames |= (mtcValue << 4); break;
        case 2: lastMTCValues.seconds = mtcValue; break;
        case 3: lastMTCValues.seconds |= (mtcValue << 4); break;
        case 4: lastMTCValues.minutes = mtcValue; break;
        case 5: lastMTCValues.minutes |= (mtcValue << 4); break;
        case 6: lastMTCValues.hours = mtcValue; break;
        case 7: 
            lastMTCValues.hours |= (mtcValue & 0x1) << 4;
            lastMTCValues.type = (mtcValue >> 1) & 0x3;
            break;
    }

    mtcCounter++;
    if (mtcCounter === 8) {
        mtcCounter = 0;
        displayTimecode(lastMTCValues);
    }
}

function handleFullFrame(data) {
    const hours = data[0];
    const minutes = data[1];
    const seconds = data[2];
    const frames = data[3];
    // The frame rate information might be encoded differently or not present
    // We'll need to determine how to extract this information from your specific implementation
    const type = 0; // Default to 24 fps for now, adjust as needed

    lastMTCValues = { hours, minutes, seconds, frames, type };
    displayTimecode(lastMTCValues);
    mtcCounter = 0; // Reset counter after full frame
}

function displayTimecode(timecode) {
    const frameRates = [24, 25, 29.97, 30];
    const frameRate = frameRates[timecode.type] || 24; // Default to 24 if unknown
    const dropFrame = (timecode.type === 2);

    let timecodeString = `${timecode.hours.toString().padStart(2, '0')}:${timecode.minutes.toString().padStart(2, '0')}:${timecode.seconds.toString().padStart(2, '0')} f${timecode.frames.toString().padStart(2, '0')}/${frameRate.toString()}`;
    
    //console.log("Displaying Timecode:", timecodeString, `(${frameRate} fps${dropFrame ? ' DF' : ''})`);
    displayOverlayMessage("Timecode", timecodeString);
}

function playbackMIDI(msg, unsafe = false, UUID = null) {
    if (session.midiIframe) {
        pokeIframeAPI("midi-in", msg, UUID);
    }
    
    if (session.midiTimecode && msg && msg.d) {
        const data = msg.d;
        if (data.length === 2 && data[0] === 241) {
            handleQuarterFrame(data[1]);
        } else if (data.length >= 10 && data[0] === 240 && data[1] === 127 && data[4] === 1) {
            if (data.length === 11) {
                handleFullFrame(data.slice(6, -1));
            } else {
                handleFullFrame(data.slice(6, 8));
            }
        }
    }
	
    if (session.midiIn === false && session.midiRemote === false) {
        return;
    } else if (session.midiOut === session.midiIn && session.midiRemote === false) {
        return;
    }
    
    log("play out");

    if (session.midiDelay && "t" in msg) {
        const timeDelay = session.midiDelay - (Date.now() - msg.t);
        if (timeDelay <= 0) {
            playOutMidi(msg);
        } else {
            setupMidiOscillator(() => playOutMidi(msg), 1000 / timeDelay);
        }
    } else {
        playOutMidi(msg);
    }

    if (unsafe) {
        return;
    } // I don't know how midi remote works in reverse, so lets ignore it

	if (session.midiRemote == 4) {
		if (msg.d[0] == 176) {
			midiHotkeysCommand(msg.d[1], msg.d[2]);
		}
	} else if (session.midiRemote == 1 || session.midiRemote == 2 || session.midiRemote == 3) {
		if (msg.d[0] == 156) {
			if (msg.d[1] == 33) {
				midiHotkeysNote("A1", msg.d[2]);
			} else if (msg.d[1] == 55) {
				midiHotkeysNote("G3", msg.d[2]);
			} else if (msg.d[1] == 57) {
				midiHotkeysNote("A3", msg.d[2]);
			} else if (msg.d[1] == 59) {
				midiHotkeysNote("B3", msg.d[2]);
			} else if (msg.d[1] == 60) {
				midiHotkeysNote("C4", msg.d[2]);
			} else if (msg.d[1] == 62) {
				midiHotkeysNote("D4", msg.d[2]);
			} else if (msg.d[1] == 64) {
				midiHotkeysNote("E4", msg.d[2]);
			} else if (msg.d[1] == 65) {
				midiHotkeysNote("F4", msg.d[2]);
			} else if (msg.d[1] == 67) {
				midiHotkeysNote("G4", msg.d[2]);
			} else if (msg.d[1] == 69) {
				midiHotkeysNote("A4", msg.d[2]);
			} else if (msg.d[1] == 43) {
				midiHotkeysNote("G2", msg.d[2]);
			} else if (msg.d[1] == 35) {
				midiHotkeysNote("B1", msg.d[2]);
			} else if (msg.d[1] == 36) {
				midiHotkeysNote("C2", msg.d[2]);
			} else if (msg.d[1] == 38) {
				midiHotkeysNote("D2", msg.d[2]);
			} else if (msg.d[1] == 40) {
				midiHotkeysNote("E2", msg.d[2]);
			} else if (msg.d[1] == 41) {
				midiHotkeysNote("F2", msg.d[2]);
			} else if (msg.d[1] == 24) {
				midiHotkeysNote("C1", msg.d[2]);
			}
		}
	}
	//var output = WebMidi.getOutputById("123456789");
	//output = WebMidi.getOutputByName("Axiom Pro 25 Ext Out");
	//output = WebMidi.outputs[0];
}


function addEventToAll(targets, trigger, callback) {
	// js helper
	const target = document.querySelectorAll(targets);
	var triggers = trigger.split(" ");
	for (let i = 0; i < target.length; i++) {
		for (let j = 0; j < triggers.length; j++) {
			setTimeout(
				function (t1, t2) {
					t1.addEventListener(t2, function (e) {
						callback(e, t1);
					});
				},
				0,
				target[i],
				triggers[j]
			);
		}
	}
}

function insertAfter(newNode, existingNode) {
	existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
}
addEventToAll(".column", "click", function (e, ele) {
	if (ele.classList.contains("skip-animation")) {
		return;
	}
	try {
		var bounding_box = ele.getBoundingClientRect();
	} catch (e) {
		return;
	}
	if (!bounding_box){
		errorlog("No bounding box for ele found");
	}
	ele.style.top = bounding_box.top + "px";
	ele.style.left = bounding_box.left - 20 + "px";
	ele.classList.add("in-animation");
	ele.classList.remove("pointer");
	ele.classList.remove("rounded");

	if (document.getElementById("empty-container")) {
		getById("empty-container").parentNode.removeChild(getById("empty-container"));
	}
	var empty = document.createElement("DIV");
	empty.id = "empty-container";
	empty.className = "column";
	ele.parentNode.insertBefore(empty, ele.nextSibling);
	const styles =
		"\
		@keyframes outlightbox {\
			0% {\
				height: 100%;\
				width: 100%;\
				top: 0px;\
				left: 0px;\
			}\
			50% {\
				height: 200px;\
				top: " +
		bounding_box.y +
		"px;\
			}\
			100% {\
				height: 200px;\
				width: " +
		bounding_box.width +
		"px;\
				top: " +
		bounding_box.y +
		"px;\
				left: " +
		bounding_box.x +
		"px;\
			}\
		}\
	";
	if (document.getElementById("lightbox-animations")) {
		getById("lightbox-animations").innerHTML = styles;
	}
	document.body.style.overflow = "hidden";
});
addEventToAll(".close", "click", function (e, ele) {
	cleanupMediaTracks();

	document.querySelectorAll(".hidden2").forEach(ele2 => {
		ele2.classList.remove("hidden2");
	});

	ele.style.display = "none";
	mapToAll(".container-inner", function (target) {
		target.style.display = "none";
	});
	document.body.style.overflow = "auto";
	var bounding_box = getById("empty-container").parentNode.getBoundingClientRect();
	setTimeout(function () {
		// just smoothes things out; breathing room to clean up things first.
		ele.parentNode.classList.add("out-animation");
	}, 1);
	ele.parentNode.style.top = bounding_box.top + "px";
	ele.parentNode.style.left = bounding_box.left + "px";
	e.stopPropagation();
});
addEventToAll(".column", "animationend", function (e, ele) {
	if (e.animationName == "inlightbox") {
		ele.classList.add("skip-animation");
		mapToAll(
			".close",
			function (target) {
				target.style.display = "block";
			},
			ele
		);
		document.querySelectorAll("#header, #miniTaskBarm, #credits, .columnfade").forEach(ele2 => {
			if (ele2 !== ele) {
				ele2.classList.add("hidden2");
			}
		});
		mapToAll(
			".container-inner",
			function (target) {
				target.style.display = "block";
			},
			ele
		);
	} else if (e.animationName == "outlightbox") {
		ele.classList.remove("in-animation");
		ele.classList.remove("out-animation");
		ele.classList.remove("skip-animation");
		ele.classList.remove("columnfade");
		ele.classList.add("pointer");
		ele.classList.add("rounded");
		getById("empty-container").parentNode.removeChild(getById("empty-container"));
		getById("lightbox-animations").sheet.deleteRule(0);
	}
});
addEventToAll("#audioSource", "mousedown touchend focusin focusout", function (e, ele) {
	var state = getById("multiselect-trigger").dataset.state || 0; // Does this return TRU instead?? GAH. #TODO:
	if (state == 0) {
		getById("multiselect-trigger").dataset.state = 1;
		getById("multiselect-trigger").classList.add("open");
		getById("multiselect-trigger").classList.remove("closed");
		mapToAll(
			".chevron",
			function (ele) {
				ele.classList.remove("bottom");
			},
			(parentElement = getById("multiselect-trigger"))
		);
		mapToAll(
			".multiselect-contents",
			function (ele) {
				ele.style.display = "block";
				mapToAll(
					'input[type="checkbox"]',
					function (ele2) {
						ele2.parentNode.style.display = "block";
						ele2.style.display = "inline-block";
					},
					ele
				);
			},
			(parentElement = getById("multiselect-trigger").parentNode)
		);
	}
	e.stopPropagation();
	//e.preventDefault();
});
addEventToAll("#audioSource3", "mousedown touchend focusin focusout", function (e, ele) {
	var state = getById("multiselect-trigger3").dataset.state || 0; // Does this return TRU instead?? GAH. #TODO:
	if (state == 0) {
		getById("multiselect-trigger3").dataset.state = 1;
		getById("multiselect-trigger3").classList.add("open");
		getById("multiselect-trigger3").classList.remove("closed");
		mapToAll(
			".chevron",
			function (target) {
				target.classList.remove("bottom");
			},
			getById("multiselect-trigger3")
		);
		mapToAll(
			".multiselect-contents",
			function (target) {
				target.style.display = "block";
			},
			getById("multiselect-trigger3").parentNode
		);
		mapToAll(
			".multiselect-contents",
			function (target) {
				mapToAll(
					'input[type="checkbox"]',
					function (target2) {
						target2.style.display = "inline-block";
						target2.parentNode.style.display = "block";
					},
					target
				);
			},
			getById("multiselect-trigger3").parentNode
		);
	}
	e.stopPropagation();
	//e.preventDefault();
});
addEventToAll("#multiselect-trigger", "mousedown touchend focusin focusout", function (e, ele) {
	var state = ele.dataset.state || 0; //  Does this return TRU instead?? GAH. #TODO:
	if (state == 0) {
		// open the dropdown
		ele.dataset.state = 1;
		ele.classList.add("open");
		ele.classList.remove("closed");
		mapToAll(
			".chevron",
			function (target) {
				target.classList.remove("bottom");
			},
			getById("multiselect-trigger")
		);
		mapToAll(
			".multiselect-contents",
			function (target) {
				target.style.display = "block";
			},
			ele.parentNode
		);
		mapToAll(
			".multiselect-contents",
			function (target) {
				mapToAll(
					'input[type="checkbox"]',
					function (target2) {
						target2.style.display = "inline-block";
						target2.parentNode.style.display = "block";
					},
					target
				);
			},
			ele.parentNode
		);
	} else {
		// close the dropdown
		ele.dataset.state = 0;
		ele.classList.add("closed");
		ele.classList.remove("open");
		mapToAll(
			".chevron",
			function (target) {
				target.classList.add("bottom");
			},
			ele
		);
		mapToAll(
			".multiselect-contents",
			function (target) {
				mapToAll(
					'input[type="checkbox"]',
					function (target2) {
						target2.style.display = "none";
						if (!target2.checked) {
							target2.parentNode.style.display = "none";
						}
					},
					target
				);
			},
			ele.parentNode
		);
	}
	e.preventDefault();
	e.stopPropagation();
});
addEventToAll("#multiselect-trigger3", "mousedown touchend focusin focusout", function (e, ele) {
	var state = ele.dataset.state || 0; // Does this return TRU instead?? GAH. #TODO:
	if (state == 0) {
		// open the dropdown
		ele.dataset.state = 1;
		ele.classList.add("open");
		ele.classList.remove("closed");
		mapToAll(
			".chevron",
			function (target) {
				target.classList.remove("bottom");
			},
			ele
		);
		mapToAll(
			".multiselect-contents",
			function (target) {
				target.style.display = "block";
			},
			ele.parentNode
		);
		mapToAll(
			".multiselect-contents",
			function (target) {
				mapToAll(
					'input[type="checkbox"]',
					function (target2) {
						target2.style.display = "inline-block";
						target2.parentNode.style.display = "block";
					},
					target
				);
			},
			ele.parentNode
		);
	} else {
		// close the dropdown
		ele.dataset.state = 0;
		ele.classList.add("closed");
		ele.classList.remove("open");
		mapToAll(
			".chevron",
			function (target) {
				target.classList.add("bottom");
			},
			ele
		);
		mapToAll(
			".multiselect-contents",
			function (target) {
				mapToAll(
					'input[type="checkbox"]',
					function (target2) {
						target2.style.display = "none";
						if (!target2.checked) {
							target2.parentNode.style.display = "none";
						}
					},
					target
				);
			},
			ele.parentNode
		);
	}
	e.preventDefault();
	e.stopPropagation();
});

function getSenders2(UUID) {
	var fixedSenders = [];
	var isAlt = false;
	if (!(UUID in session.pcs)) {
		return fixedSenders;
	}
	if ("realUUID" in session.pcs[UUID]) {
		isAlt = true;
		UUID = session.pcs[UUID].realUUID;
		if (!(UUID in session.pcs)) {
			return fixedSenders;
		}
	}
	var senders = session.pcs[UUID].getSenders();

	if (isAlt) {
		senders.forEach(sender => {
			if (sender.track && sender.track.id) {
				if (sender.track.id in screenshareTracks) {
					// I'm not going to change track.kind, since OBS isn't part of this list
					fixedSenders.push(sender);
				}
			}
		});
	} else {
		senders.forEach(sender => {
			if (sender.track && sender.track.id) {
				if (!(sender.track.id in screenshareTracks)) {
					fixedSenders.push(sender);
				}
			}
		});
	}

	return fixedSenders;
}

function getReceivers2(UUID) {
	var fixedReceivers = [];
	var isAlt = false;
	var ssTracks = [];
	if ("realUUID" in session.rpcs[UUID]) {
		isAlt = true;
		UUID = session.rpcs[UUID].realUUID;
		if (!("screenIndexes" in session.rpcs[UUID])) {
			errorlog("this is supposed to be a screen share, but no screen share index was found");
			return;
		}
		ssTracks = session.rpcs[UUID].screenIndexes;
	} else if ("screenIndexes" in session.rpcs[UUID] && session.rpcs[UUID].screenIndexes) {
		ssTracks = session.rpcs[UUID].screenIndexes;
	}

	if (session.rpcs[UUID] && session.rpcs[UUID].getReceivers) {
		var receivers = session.rpcs[UUID].getReceivers();
	} else {
		var receivers = [];
	}

	try {
		if (session.rpcs[UUID].whep && session.rpcs[UUID].whep.getReceivers) {
			// used to be "mc", not "whep"
			try {
				receivers = receivers.concat(session.rpcs[UUID].whep.getReceivers());
			} catch (e) {
				errorlog(e);
			}
		}
	} catch (e) {
		errorlog(e);
	}

	if (isAlt) {
		for (var i = 0; i < receivers.length; i++) {
			for (var j = 0; j < ssTracks.length; j++) {
				if (i == ssTracks[j]) {
					fixedReceivers.push(receivers[i]);
					break;
				}
			}
		}
	} else {
		for (var i = 0; i < receivers.length; i++) {
			var matched = false;
			for (var j = 0; j < ssTracks.length; j++) {
				if (i == ssTracks[j]) {
					matched = true;
				}
			}
			if (!matched) {
				fixedReceivers.push(receivers[i]);
			}
		}
	}

	return fixedReceivers;
}

function getReceiversMC(UUID) {
	var fixedReceivers = [];
	var isAlt = false;
	var ssTracks = [];
	if ("realUUID" in session.rpcs[UUID]) {
		isAlt = true;
		UUID = session.rpcs[UUID].realUUID;
		if (!("screenIndexes" in session.rpcs[UUID])) {
			errorlog("this is supposed to be a screen share, but no screen share index was found");
			return;
		}
		ssTracks = session.rpcs[UUID].screenIndexes;
	} else if ("screenIndexes" in session.rpcs[UUID] && session.rpcs[UUID].screenIndexes) {
		ssTracks = session.rpcs[UUID].screenIndexes;
	}

	receivers = [];
	if (session.rpcs[UUID].whep) {
		receivers = session.rpcs[UUID].whep.getReceivers();
	}

	if (isAlt) {
		for (var i = 0; i < receivers.length; i++) {
			for (var j = 0; j < ssTracks.length; j++) {
				if (i == ssTracks[j]) {
					fixedReceivers.push(receivers[i]);
					break;
				}
			}
		}
	} else {
		for (var i = 0; i < receivers.length; i++) {
			var matched = false;
			for (var j = 0; j < ssTracks.length; j++) {
				if (i == ssTracks[j]) {
					matched = true;
				}
			}
			if (!matched) {
				fixedReceivers.push(receivers[i]);
			}
		}
	}
	return fixedReceivers;
}

async function createSecondStream2(UUID) {
	if (session.pcs[UUID].allowScreenVideo === false && session.pcs[UUID].allowScreenAudio === false) {
		return false;
	}
	if ("realUUID" in session.pcs[UUID]) {
		return false;
	} // we don't want to attach to an existing screen share obviously
	if (!session.screenStream) {
		return false;
	}

	if (!(UUID + "_screen" in session.pcs)) {
		warnlog(UUID + "_screen; new screen link");
		session.pcs[UUID + "_screen"] = {};
		session.pcs[UUID + "_screen"].realUUID = UUID;
		session.pcs[UUID + "_screen"].stats = {};
		session.pcs[UUID + "_screen"].sceneDisplay = null;
		session.pcs[UUID + "_screen"].sceneMute = null;
		session.pcs[UUID + "_screen"].solo = null;
		session.pcs[UUID + "_screen"].allowVideo = session.pcs[UUID].allowScreenVideo;
		session.pcs[UUID + "_screen"].allowAudio = session.pcs[UUID].allowScreenAudio;
		session.pcs[UUID + "_screen"].allowDrawing = session.pcs[UUID].allowDrawing;
		if (session.pcs[UUID + "_screen"].allowDrawing){
			if (session.screenShareElement && session.screenShareElement.syncDrawOnVideo){
				session.screenShareElement.syncDrawOnVideo();
			}
		}
		session.pcs[UUID + "_screen"].layout = null;
		session.pcs[UUID + "_screen"].obsState = {};
		session.pcs[UUID + "_screen"].obsState.visibility = null;
		session.pcs[UUID + "_screen"].obsState.sourceActive = null;
		session.pcs[UUID + "_screen"].obsState.streaming = null;
		session.pcs[UUID + "_screen"].obsState.recording = null;
		session.pcs[UUID + "_screen"].obsState.virtualcam = null;
		session.pcs[UUID + "_screen"].optimizedBitrate = false;
		session.pcs[UUID + "_screen"].savedBitrate = false;
		session.pcs[UUID + "_screen"].bitrateTimeout = null;
		session.pcs[UUID + "_screen"].bitrateTimeoutFirefox = false;
		session.pcs[UUID + "_screen"].setAudioBitrate = false;
		session.pcs[UUID + "_screen"].setBitrate = false;
		session.pcs[UUID + "_screen"].maxBandwidth = null; // based on max available bitrate
		session.pcs[UUID + "_screen"].limitAudio = false;
		session.pcs[UUID + "_screen"].audioMutedOverride = false;
		session.pcs[UUID + "_screen"].enhanceAudio = false;
		session.pcs[UUID + "_screen"].meshcast = null;
		session.pcs[UUID + "_screen"].UUID = UUID + "_screen";
		session.pcs[UUID + "_screen"].scale = false;
		session.pcs[UUID + "_screen"].scaleDueToBitrate = false;
		session.pcs[UUID + "_screen"].scaleWidth = false;
		session.pcs[UUID + "_screen"].scaleHeight = false;
		session.pcs[UUID + "_screen"].scaleSnap = false;
		session.pcs[UUID + "_screen"].scaleResolution = false;
		session.pcs[UUID + "_screen"].scene = false;
		session.pcs[UUID + "_screen"].keyframeRate = false;
		session.pcs[UUID + "_screen"].keyframeTimeout = null;
		session.pcs[UUID + "_screen"].label = false;
		session.pcs[UUID + "_screen"].order = false;
		session.pcs[UUID + "_screen"].preferVideoCodec = false;
		session.pcs[UUID + "_screen"].startTime = Date.now();
		session.pcs[UUID + "_screen"].needsPublishing = null;
		// session.pcs[UUID+"_screen"].rotation = false; I don't think this will ever be used?

		// we will use allowVideo/allowAudio from the main UUID parent

		session.pcs[UUID + "_screen"].getStats = function () {
			return new Promise((resolve, reject) => {
				resolve([]);
			});
		};
	}

	/* if (session.audioContentHint && tracks.length){
		tracks.forEach(trk=>{
			try {
				
				trk.contentHint = session.audioContentHint;
			} catch(e){
				errorlog(e);
			}
		});
	} */

	var senders = getSenders2(UUID + "_screen");
	var tracks = session.screenStream.getTracks();

	for (var i = 0; i < tracks.length; i++) {
		var track = tracks[i];

		try {
			if (track.kind === "audio" && session.pcs[UUID + "_screen"].allowAudio === false) {
				continue;
			} else if (track.kind === "video" && session.pcs[UUID + "_screen"].allowVideo === false) {
				continue;
			}
		} catch (e) {
			errorlog(e);
		}

		if (session.audioContentHint && track.kind === "audio") {
			try {
				track.contentHint = session.audioContentHint; // this gets triggered too often I think
			} catch (e) {
				errorlog(e);
			}
		}

		if (session.screenshareContentHint && track.kind === "video") {
			try {
				track.contentHint = session.screenshareContentHint; // this gets triggered too often I think
			} catch (e) {
				errorlog(e);
			}
		} else if (session.contentHint && track.kind === "video") {
			try {
				track.contentHint = session.contentHint; // this gets triggered too often I think
			} catch (e) {
				errorlog(e);
			}
		}

		var added = false;
		for (var j = 0; j < senders.length; j++) {
			// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
			var sender = senders[j];
			if (sender.track && sender.track.kind == track.kind) {
				sender.replaceTrack(track); // replace may not be supported by all browsers.  eek.
				sender.track.enabled = true;
				added = true;
				break;
			}
		}
		if (!added) {
			session.pcs[UUID].addTrack(track, session.screenStream);
		}
	}

	session.applyIsolatedChat();

	updateMixer();
}

var screenshareTracks = {};
var firsttime = true;
async function createSecondStream() {
	////////////////////////////  &sstype=3 ?
	if (session.screenShareState == false) {
		// adding a screen

		var video = {};

		var quality = session.quality_ss;

		if (quality === false) {
			quality = session.roomid ? session.quality_room : session.quality_wb;
		}

		if (session.quality !== false) {
			quality = session.quality;
		}
		if (session.screensharequality !== false) {
			quality = session.screensharequality;
		}

		if (quality == -1) {
			// unlocked capture resolution
		} else if (quality == -2) {
			video.width = {
				ideal: 3840
			};
			video.height = {
				ideal: 2160
			};
		} else if (quality == -3) {
			video.width = {
				ideal: 2560
			};
			video.height = {
				ideal: 1440
			};
		} else if (quality == 0) {
			video.width = {
				ideal: 1920
			};
			video.height = {
				ideal: 1080
			};
		} else if (quality == 1) {
			video.width = {
				ideal: 1280
			};
			video.height = {
				ideal: 720
			};
		} else if (quality == 2) {
			video.width = {
				ideal: 640
			};
			video.height = {
				ideal: 360
			};
		} else if (quality >= 3) {
			// lowest
			video.width = {
				ideal: 320
			};
			video.height = {
				ideal: 180
			};
		}

		if (session.width) {
			video.width = {
				ideal: session.width
			};
		}
		if (session.height) {
			video.height = {
				ideal: session.height
			};
		}

		var constraints = {
			// this part is a bit annoying. Do I use the same settings?  I can add custom setting controls here later
			audio: {
				echoCancellation: true, // we want to cancel echo, since this is a secondary stream
				autoGainControl: false,
				noiseSuppression: false
			},
			video: video
			//,cursor: {exact: "none"}
		};

		try {
			let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
			if (supportedConstraints.cursor) {
				if (session.screensharecursor) {
					constraints.video.cursor = ["always", "motion"];
				} else {
					constraints.video.cursor = "never";
				}
			}
			if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) {
				constraints.audio.suppressLocalAudioPlayback = true;
			}
			//
			if (session.preferCurrentTab) {
				constraints.preferCurrentTab = true;
			}
			if (session.selfBrowserSurface) {
				constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include
			}
			if (session.surfaceSwitching) {
				constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include
			}
			if (session.systemAudio) {
				constraints.systemAudio = session.systemAudio; // exclude or include
			}
			if (session.displaySurface && supportedConstraints.displaySurface) {
				constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser
			}
		} catch (e) {
			warnlog("navigator.mediaDevices.getSupportedConstraints() not supported");
		}

		if (session.echoCancellation === false) {
			constraints.audio.echoCancellation = false;
		}
		if (session.autoGainControl === true) {
			constraints.audio.autoGainControl = true;
		}
		if (session.noiseSuppression === true) {
			constraints.audio.noiseSuppression = true;
		}
		if (session.voiceIsolation === true){
			constraint.audio.voiceIsolation = true;
		}
		//if (audio == false) {
		//	constraints.audio = false;
		//}

		var overrideFramerate = false;
		if (session.frameRate !== false && session.maxframeRate != false) {
			overrideFramerate = session.frameRate;
			constraints.video.frameRate = {
				ideal: session.maxframeRate,
				max: session.maxframeRate
			};
		} else if (session.frameRate !== false) {
			constraints.video.frameRate = session.frameRate;
		} else if (session.maxframeRate != false) {
			constraints.video.frameRate = {
				ideal: session.maxframeRate,
				max: session.maxframeRate
			};
		} else {
			constraints.video.frameRate = {
				ideal: 60
			};
		}

		if (session.screenshareVideoOnly) {
			constraints.audio = false;
		}

		if (session.forceAspectRatio) {
			// await updateCameraConstraints("aspectRatio", session.forceAspectRatio);
			if (constraints.video && constraints.video !== true) {
				constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) };

				if (constraints.video.width && !session.width) {
					delete constraints.video.width;
				} else if (constraints.video.height && !session.height) {
					delete constraints.video.height;
				}
			}
		}

		if (constraints.video !== false && Object.keys(constraints.video).length == 0) {
			constraints.video = true;
		}

		if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
			if (!ElectronDesktopCapture) {
				if (!session.cleanOutput) {
					warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)");
				}
				return false;
			}
		}

		log("sstype3 screen share");
		log(constraints);

		navigator.mediaDevices
			.getDisplayMedia(constraints)
			.then(async function (stream) {
				try {
					var constraint = {};
					if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) {
						constraint.aspectRatio = parseFloat(session.forceAspectRatio);
					} else if (session.forceScreenShareAspectRatio) {
						constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio);
					}
					if (overrideFramerate) {
						constraint.frameRate = overrideFramerate;
					}
					if (Object.keys(constraint).length) {
						await stream.getVideoTracks()[0].applyConstraints({
							advanced: [constraint]
						});
						log({
							advanced: [constraint]
						});
					}
				} catch (e) {
					errorlog(e);
				}

				session.screenShareState = true;
				session.screenStream = stream;
				pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);

				//if (!session.screenVideoElement){
				//	session.screenVideoElement = createVideoElement()
				//}
				try {
					stream.getVideoTracks()[0].onended = function () {
						stopSecondScreenshare();
					};
				} catch (e) {
					log("No Video selected; screensharing?");
				}

				session.screenStream.getTracks().forEach(function (track) {
					screenshareTracks[track.id] = true; // obs isn't included, so no point to check track.kind
				});
				for (UUID in session.pcs) {
					createSecondStream2(UUID);
				}

				if (!firsttime) {
					var msg = {};
					msg.screenStopped = false;
					session.sendMessage(msg);
				} else if (!session.screenShareElement) {
					session.screenShareElement = createVideoElement();
					session.screenShareElement.muted = true;
					session.screenShareElement.autoplay = true;
					session.screenShareElement.controls = session.showControls || false;

					session.screenShareElement.id = "screensharesource";
					session.screenShareElement.dataset.sid = session.streamID + ":s";

					if (typeof session.volume == "number") {
						session.screenShareElement.volume = session.volume;
					} else {
						session.screenShareElement.volume = 1.0; // play audio automatically
					}
					session.screenShareElement.classList.add("tile");
					session.screenShareElement.setAttribute("playsinline", "");
					session.screenShareElement.controlTimer = null;

					session.screenShareElement.dataset.menu = "context-menu-video";
					if (!session.cleanOutput) {
						session.screenShareElement.classList.add("task"); // this adds the right-click menu
					}
					createDirectorScreenshareOnlyBox();

					if (document.getElementById("videoScreenContainer_director")) {
						getById("videoScreenContainer_director").appendChild(session.screenShareElement);
					}

					session.screenShareElement.onpause = event => {
						// prevent things from pausing; human or other
						if (!(event.ctrlKey || event.metaKey)) {
							log("Video paused; auto playing");
							event.currentTarget
								.play()
								.then(_ => {
									log("playing 10");
								})
								.catch(warnlog);
						}
					};

					session.screenShareElement.addEventListener("click", function (e) {
						log("click");
						try {
							if (e.ctrlKey || e.metaKey) {
								e.preventDefault();

								var [menu, innerMenu] = statsMenuCreator();

								menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu, true);

								printMyStats(innerMenu, true);
								e.stopPropagation();
								return false;
							}
						} catch (e) {
							errorlog(e);
						}
					});

					session.screenShareElement.touchTimeOut = null;
					session.screenShareElement.touchLastTap = 0;
					session.screenShareElement.touchCount = 0;

					session.screenShareElement.addEventListener("touchend", function (event) {
						if (session.disableMouseEvents) {
							return;
						}
						log("touched");

						//document.ontouchup = null;
						//document.onmouseup = null;
						document.onmousemove = null;
						document.ontouchmove = null;

						var currentTime = new Date().getTime();
						var tapLength = currentTime - session.screenShareElement.touchLastTap;
						clearTimeout(session.screenShareElement.touchTimeOut);
						if (tapLength < 500 && tapLength > 0) {
							///
							log("double touched");
							session.screenShareElement.touchCount += 1;
							event.preventDefault();
							if (session.screenShareElement.touchCount < 5) {
								session.screenShareElement.touchLastTap = currentTime;
								return false;
							}
							session.screenShareElement.touchLastTap = 0;
							session.screenShareElement.touchCount = 0;

							var [menu, innerMenu] = statsMenuCreator();

							menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu, true);

							printMyStats(innerMenu, true);
							event.stopPropagation();
							return false;
							//////
						} else {
							session.screenShareElement.touchCount = 1;
							session.screenShareElement.touchLastTap = currentTime;

							session.screenShareElement.touchTimeOut = setTimeout(
								function (vv) {
									clearTimeout(vv.touchTimeOut);
									vv.touchLastTap = 0;
									vv.touchCount = 0;
								},
								5000,
								session.screenShareElement
							);
						}
					});
				}

				firsttime = false;

				session.screenShareElement.srcObject = session.screenStream;

				getById("screensharebutton").classList.add("green");
				getById("screensharebutton").ariaPressed = "true";
				getById("screensharebutton").title = getTranslation("stop-screen-sharing");

				getById("screenshare2button").classList.add("green");
				getById("screenshare2button").ariaPressed = "true";
				getById("screenshare2button").title = getTranslation("stop-screen-sharing");

				getById("screenshare3button").classList.add("green");
				getById("screenshare3button").ariaPressed = "true";
				getById("screenshare3button").title = getTranslation("stop-screen-sharing");

				if (session.autorecord || session.autorecordlocal) {
					log("AUTO RECORD START SSTYPE3");
					setTimeout(
						function (s) {
							if (!session.screenStream) {
								return;
							}
							try {
								var ele = document.getElementById("recordLocalScreenbutton");
								if (ele) {
									ele.classList.add("red");
									ele.classList.remove("hidden");
									if (!ele.vid) {
										var v = createVideoElement();
										v.muted = true;
										v.srcObject = s;
										ele.vid = v;
									}
									if (ele.vid.recorder || ele.vid.recording) {
										ele.vid.recorder.stop();
										ele.classList.remove("red");
										ele.classList.add("hidden");
										ele.vid = null;
									} else {
										var videoKbps = session.recordDefault;
										if (session.recordLocal !== false) {
											videoKbps = session.recordLocal;
										}
										recordLocalVideo(null, videoKbps, ele.vid);
									}
								}
							} catch (e) {
								errorlog(e);
							}
						},
						2000,
						session.screenStream
					);
				}

				setTimeout(function () {
					updateMixer();
				}, 100);

				setTimeout(function () {
					updateMixer();
				}, 1000);
			})
			.catch(function (err) {
				errorlog(err);
			});
	} else {
		// removing a screen
		stopSecondScreenshare();
	}
}
function recordLocalScreenStopRecord() {
	var ele = document.getElementById("recordLocalScreenbutton");
	if (ele) {
		try {
			ele.classList.remove("red");
			ele.classList.add("hidden");
			if (ele.vid) {
				if (ele.vid.recorder || ele.vid.recording) {
					ele.vid.recorder.stop();
				}
				ele.vid = null;
			}
		} catch (e) {
			errorlog(e);
		}
	}
}
function stopSecondScreenshare() {
	var msg = {};
	msg.screenStopped = true;
	session.sendMessage(msg);

	var ele = document.getElementById("recordLocalScreenbutton");
	if (ele) {
		try {
			ele.classList.remove("red");
			ele.classList.add("hidden");
			if (ele.vid) {
				if (ele.vid.recorder || ele.vid.recording) {
					ele.vid.recorder.stop();
				}
				ele.vid = null;
			}
		} catch (e) {
			errorlog(e);
		}
	}
	if (session.screenStream){
		session.screenStream.getTracks().forEach(function (track) {
			// previous video track; saving it. Must remove the track at some point.
			for (UUID in session.pcs) {
				if (!("realUUID" in session.pcs[UUID])) {
					continue;
				} // not a screen share, so skip
				var senders = getSenders2(UUID);
				senders.forEach(sender => {
					// I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
					if (sender.track && sender.track.kind == "video") {
						sender.track.enabled = false;
					}
				});
			}
			if (track.id in screenshareTracks) {
				// obs isn't included, so no point to check track.kind
				session.screenStream.removeTrack(track);
				track.stop();
				screenshareTracks[track.id] = false;
			}
		});
	}
	if (document.getElementById("container_screen_director")){
		document.getElementById("container_screen_director")
	}
	
	session.screenStream = false;
	session.screenShareState = false;
	pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);

	getById("screensharebutton").classList.remove("green");
	getById("screensharebutton").ariaPressed = "false";
	getById("screensharebutton").title = getTranslation("share-a-screen");

	getById("screenshare2button").classList.remove("green");
	getById("screenshare2button").ariaPressed = "false";
	getById("screenshare2button").title = getTranslation("share-a-screen");

	getById("screenshare3button").classList.remove("green");
	getById("screenshare3button").ariaPressed = "false";
	getById("screenshare3button").title = getTranslation("share-a-screen");
	
	if (document.getElementById("screensharesource")){
		document.getElementById("screensharesource").load()
	}

	setTimeout(function () {
		updateMixer();
	}, 100);

	setTimeout(function () {
		updateMixer();
	}, 1000);
}

function enableFullscreenZoom(){
	const content = document.getElementById('gridlayout');
	let lastScrollPosition = { x: 0, y: 0 };
	content.style.overflow = "visible";
	content.style.transformOrigin = "center center"; // Set transform origin to center

	// Add styles for the container to allow centering
	content.style.position = "relative";
	content.parentElement.style.display = "flex";
	content.parentElement.style.justifyContent = "center";
	content.parentElement.style.alignItems = "center";
	content.parentElement.style.minHeight = "100vh";

	content.parentNode.insertAdjacentHTML('afterbegin',
	'<div class="zoom-control" style="padding:20px 40px;z-index:9999;position:fixed;bottom:0px;left:0;width:100%;">\
		<input type="range" \
			   id="zoomSlider2"\
			   min="100"\
			   max="400"\
			   value="100"\
			   step="10">\
	</div>');

	const viewport = document.querySelector('meta[name="viewport"]');

	viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=4.0, user-scalable=yes';
	if (!viewport) {
		const meta = document.createElement('meta');
		meta.name = 'viewport';
		meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=4.0, user-scalable=yes';
		document.head.appendChild(meta);
	}

	const slider = document.getElementById('zoomSlider2');
	slider.addEventListener('input', (e) => {
		const scale = e.target.value / 100;
		
		// Calculate the center point before scaling
		const rect = content.getBoundingClientRect();
		const centerX = rect.left + rect.width / 2;
		const centerY = rect.top + rect.height / 2;
		
		// Apply the transform with translate to maintain center point
		content.style.transform = `scale(${scale})`;
		
		// Calculate scroll position to keep center point
		const newWidth = rect.width * scale;
		const newHeight = rect.height * scale;
		const newLeft = centerX - newWidth / 2;
		const newTop = centerY - newHeight / 2;
		
		window.scrollTo(
			window.scrollX + (newLeft - rect.left),
			window.scrollY + (newTop - rect.top)
		);
	});
}