/*
* Copyright (c) 2026 Steve Seguin. All Rights Reserved.
*
* Use of this source code is governed by the APGLv3 open-source license
* that can be found in the LICENSE file in the root of the source
* tree. Alternative licencing options can be made available on request.
*
*/
/*jshint esversion: 6 */
///// 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": "You are not the director of this room. You will have limited to no control. See &codirector on how to become a co-director.",
"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": "Tip: Use headphones to avoid audio echo issues.",
"camera-tip-c922": "Tip: 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": "Tip: 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 surround sound drivers may cause problems",
"prompt-access-request": " is trying to view your stream. Allow them?",
"confirm-reload-user": "Are you sure you wish to reload this user's browser?",
"webrtc-is-blocked": "⚠ This browser has either blocked WebRTC or does not support it.\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 this guide for an alternative method to do so.",
"mobile-no-screen-share": "Sorry, your mobile browser does not support screen-sharing.\n\nThe The native apps 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": "Tip: Blue Yeti microphones may experience issues being overly loud. Please see here 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": "
Notice: The system cannot be accessed or is currently slow to respond.
\nIf a routing issue, try adding &proxy to the URL; you can also try https://proxy.vdo.ninja 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: https://backup.vdo.ninja\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.
Please see the documention 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
",
"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. 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 () 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("-", " "); //
}
}
// Extract hostname from TURN server URL for QoS tracking
// Handles formats: turn:host:port, turns:host:port, turn:user@host:port, turns:[ipv6]:port
function extractTurnHostnameFromUrl(turnUrl) {
if (!turnUrl || typeof turnUrl !== "string") return null;
var cleaned = turnUrl.replace(/^turns?:/i, "");
cleaned = cleaned.split("?")[0];
var atIndex = cleaned.lastIndexOf("@");
if (atIndex !== -1) cleaned = cleaned.slice(atIndex + 1);
if (cleaned[0] === "[") {
var end = cleaned.indexOf("]");
return end !== -1 ? cleaned.slice(1, end) : null;
}
return cleaned.split(":")[0] || null;
}
// Build QoS allowlist from a list of TURN server configurations
function buildQosTurnAllowlist(turnlist) {
var allowlist = [];
if (!turnlist || !Array.isArray(turnlist)) return allowlist;
turnlist.forEach(function(turn) {
var urls = turn.urls || turn.url || [];
if (typeof urls === "string") urls = [urls];
urls.forEach(function(u) {
var host = extractTurnHostnameFromUrl(u);
if (host && !allowlist.includes(host)) {
allowlist.push(host);
}
});
});
return allowlist;
}
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 getSlotColor(index) {
var slotColorPalette = [
"#00AAAA",
"#FF0000",
"#0000FF",
"#AA00AA",
"#00FF00",
"#AAAA00",
"#AACC44",
"#CCAA44",
"#CC44AA",
"#44AACC"
];
index = parseInt(index);
if (!isFinite(index) || index < 0) {
index = 0;
}
if (!slotColorPalette[index]) {
var hue = (index * 47) % 360;
slotColorPalette[index] = "hsl(" + hue + ", 65%, 55%)";
}
return slotColorPalette[index];
}
function getSlotColorBySlotNumber(slotNumber) {
var slotIndex = parseInt(slotNumber);
if (!isFinite(slotIndex) || slotIndex <= 0) {
return null;
}
return getSlotColor(slotIndex - 1);
}
function applySlotColor(element, slotNumber) {
if (!element) {
return;
}
var color = getSlotColorBySlotNumber(slotNumber);
if (color) {
element.style.background = color;
element.style.backgroundColor = color;
} else {
element.style.removeProperty("background");
element.style.removeProperty("background-color");
}
}
function populateSlotPicker(maxSlots) {
var picker = document.getElementById("slotPicker");
if (!picker) {
return;
}
var html = "
Assign to slot:
";
html += '
Unset
';
for (var i = 1; i <= session.maxAvailableSlots; i++) {
var color = getSlotColor(i - 1);
html += '
Slot ' + i + "
";
}
picker.innerHTML = html;
}
function positionAlertModalNearEvent(modal, event) {
if (!modal || !event) {
return;
}
try {
var margin = 16;
var scrollX = window.pageXOffset || document.documentElement.scrollLeft || 0;
var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
modal.classList.add("alertModal--anchored");
modal.style.transform = "none";
var modalWidth = modal.offsetWidth || 0;
var modalHeight = modal.offsetHeight || 0;
var desiredLeft = event.pageX - modalWidth / 2;
var desiredTop = event.pageY + 24;
if (!isFinite(desiredLeft)) {
desiredLeft = scrollX + margin;
}
if (!isFinite(desiredTop)) {
desiredTop = scrollY + margin;
}
var maxLeft = scrollX + viewportWidth - modalWidth - margin;
var maxTop = scrollY + viewportHeight - modalHeight - margin;
modal.style.left = Math.max(scrollX + margin, Math.min(maxLeft, desiredLeft)) + "px";
modal.style.top = Math.max(scrollY + margin, Math.min(maxTop, desiredTop)) + "px";
} catch (err) {
errorlog(err);
}
}
(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: https://reports.vdo.ninja/?name=" + recordResults + "", 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) {
return true;
}
if ((iOS || iPad) && SafariVersion < 16.4) {
return true;
}
if (typeof session !== "undefined") {
if (session.forceLegacyWakeLock) {
return true;
}
if (session.wakeLockActive) {
return false;
}
}
return true;
}
} catch (e) { }
return true; // No Wake Lock API or no active lock; need legacy keep alive for mobile
};
function removeLegacyKeepAlivePlayer() {
const keepAlive = document.getElementById("keepAlivePlayer");
if (keepAlive) {
keepAlive.remove();
}
}
function ensureLegacyKeepAlivePlayer(ignoreVideoPresence = false) {
if (typeof session === "undefined" || !session.mobile) {
removeLegacyKeepAlivePlayer();
return false;
}
if (!needsLegacyWakeLock()) {
removeLegacyKeepAlivePlayer();
return false;
}
if (!session.streamSrc) {
return true;
}
try {
if (
!ignoreVideoPresence &&
session.streamSrc.getVideoTracks &&
session.streamSrc.getVideoTracks().length
) {
removeLegacyKeepAlivePlayer();
return false;
}
} catch (e) {
errorlog(e);
}
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);
}
return true;
}
function startLegacyKeepAliveLoop(ignoreVideoPresence = false) {
if (typeof session === "undefined" || !session.mobile) {
return;
}
if (session.keepAliveInterval) {
if (ignoreVideoPresence) {
session.keepAliveIgnoreVideo = true;
}
return;
}
session.keepAliveIgnoreVideo = !!ignoreVideoPresence;
const runner = () => {
const keepRunning = ensureLegacyKeepAlivePlayer(session.keepAliveIgnoreVideo);
if (!keepRunning) {
clearInterval(session.keepAliveInterval);
session.keepAliveInterval = null;
session.keepAliveIgnoreVideo = false;
}
};
const shouldContinue = ensureLegacyKeepAlivePlayer(session.keepAliveIgnoreVideo);
if (!shouldContinue) {
session.keepAliveIgnoreVideo = false;
return;
}
session.keepAliveInterval = setInterval(runner, 4000);
}
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();
}
var deleteOldMediaTimeout = null;
function deleteOldMedia(timed = false) {
if (!timed) {
if (!deleteOldMediaTimeout) {
deleteOldMediaTimeout = setTimeout(function () {
deleteOldMediaTimeout = null;
deleteOldMedia(true);
}, 2000);
}
return;
}
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 = `
×
`;
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 = "" + inputText.replace("\n", " ") + "";
inputText = inputText.replace(/\n/g, " ");
var type = "text";
if (asterix) {
type = "password";
}
if (time) {
modalTemplate = `
×${inputText}
minutes,
seconds
Count up from zero instead
`;
} else if (recording) {
modalTemplate = `
×${inputText}
Upload to your Google Drive
Upload to your Drop Box
start record
`;
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 = "" + inputText.replace("\n", " ") + "";
inputText = inputText.replace(/\n/g, " ");
var type = "text";
if (asterix) {
type = "password";
}
if (time) {
modalTemplate = `
×${inputText}
minutes,
seconds
Count up from zero instead
`;
} else if (recording) {
modalTemplate = `
×${inputText}
Upload to your Google Drive
Upload to your Drop Box
`;
} else if (hotkey) {
modalTemplate = `
×${inputText}
`;
} else {
modalTemplate = `
×${inputText}
`;
}
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 = "" + inputText.replace("\n", " ") + "";
inputText = inputText.replace(/\n/g, " ");
var type = "text";
modalTemplate = `
×${inputText}
Upload to your Google Drive
Upload to your Drop Box
`;
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 = "" + getTranslation("transfer-guest-to-room").replace("\n", " ") + "";
inputText = inputText.replace(/\n/g, " ");
modalTemplate = `
×${inputText} Allow the guest to rejoin the transfer room on their own Guest will arrive in the new room in broadcast mode Guest will arrive in the new room in queue mode
`;
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.queueType == 4) {
// For both hold modes, publish to any deferred peers upon activation.
// queueType 3: All peers were deferred (needsPublishing=true)
// queueType 4: Only non-director peers were deferred; directors already receiving
closeModal(false, "123");
for (var UUID in session.pcs) {
if (session.pcs[UUID].needsPublishing) {
session.initialPublish(UUID);
}
}
}
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, context = 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;
Prompts[promptID].context = context;
var zindex = 33 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length;
if (block) {
var backdropClass = "opaqueBackdrop";
} else {
var backdropClass = "modalBackdrop";
}
inputText = "" + inputText.replace("\n", " ") + "";
inputText = inputText.replace(/\n/g, " ");
modalTemplate = `
]/g, "") : ''}" style="z-index:${zindex + 2}">
×${inputText}
]/g, "") : ''}" style="z-index:${zindex + 1}">
`;
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;
}
async function confirmHangupWithBlock(inputText) {
// Similar to confirmAlt but includes a "block from rejoining" checkbox
// Returns: { confirmed: boolean, block: boolean }
var result = { confirmed: false, block: false };
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;
var backdropClass = "modalBackdrop";
inputText = "" + inputText.replace("\n", " ") + "";
inputText = inputText.replace(/\n/g, " ");
modalTemplate = `
×${inputText}
`;
document.body.insertAdjacentHTML("beforeend", modalTemplate);
document.getElementById("submit_" + promptID).focus();
document.getElementById("submit_" + promptID).addEventListener("click", function (event) {
var pid = event.target.dataset.pid;
result.confirmed = true;
result.block = document.getElementById("blockUser_" + pid).checked;
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, " ");
} catch (e) {
errorlog(message);
}
if (!modalID) {
modalID = Math.floor(Math.random() * 999) + 1000;
}
modalTemplate = `
×${message}
`;
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) {
// Check for alertModal with data-modalID (warnUser modals)
var alertModalMatch = document.querySelector("#alertModal[data-modalID='" + modalID + "']");
// Check for promptModal with data-context (confirmAlt modals like approval popups)
var ctx = ("" + modalID).replace(/["<>]/g, "");
var promptModalMatch = document.querySelector('.promptModal[data-context="' + ctx + '"]');
if (promptModalMatch) {
// Close the confirmAlt modal by context (programmatic dismissal)
try {
var modalIdAttr = promptModalMatch.id; // e.g., "modal_pid_abc123"
if (modalIdAttr && modalIdAttr.startsWith("modal_")) {
var pid = modalIdAttr.substring(6); // Extract "pid_abc123"
// Remove modal and backdrop
promptModalMatch.remove();
var backdrop = document.getElementById("modalBackdrop_" + pid);
if (backdrop) { backdrop.remove(); }
// Do NOT resolve the promise - let it stay pending
// Resolving would trigger the denial dialog in promptApproval
if (typeof Prompts !== "undefined" && Prompts[pid]) {
delete Prompts[pid]; // Clean up reference, but don't resolve
}
} else {
promptModalMatch.remove();
}
} catch (e) { warnlog(e); }
return;
}
if (!alertModalMatch) {
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 = "" + getTranslation("share-with-trusted") + "";
} else {
target.innerHTML = "" + getTranslation("pass-recommended") + "";
}
} else {
target.innerHTML = "" + getTranslation("insecure-room-name") + " " + getTranslation("allowed-chars") + ": A-Z, a-z, 0-9, _";
}
};
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, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
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 = Object.assign({}, session.obsState); // shallow copy to avoid mutating global state
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 = ``;
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;
var tallyStyle = session.tallyStyle;
if (!tallyStyle && session.tallyStyleDefault) {
tallyStyle = session.tallyStyleDefault;
}
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 (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 (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 (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 (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 (tallyStyle) {
getById("main").classList.add("onair");
}
} else {
getById("obsState").classList.remove("onair");
if (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");
}
// Keep the toolbar button visually hot while guests are waiting
var queueButton = getById("queuebutton");
if (queueButton) {
if (session.queueList.length) {
queueButton.classList.add("queueAttention");
} else {
queueButton.classList.remove("queueAttention");
}
}
var queueBadge = getById("queueNotification");
if (queueBadge) {
if (session.queueList.length) {
queueBadge.classList.add("queueNotificationPulse");
} else {
queueBadge.classList.remove("queueNotificationPulse");
}
}
if (adding) {
if (session.beepToNotify) {
// Favor the louder knock tone when approvals are enabled
playtone(false, session.knockToneEnabled ? "knocktone" : "testtone");
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");
}
};
session.requestAutofocusChange = async function (enabled, UUID, passwd = session.remote) {
log("request autofocus change: " + enabled);
var msg = {};
msg.autofocus = enabled;
msg.remote = passwd;
msg = await session.encodeRemote(msg);
if (session.sendRequest(msg, UUID)) {
log("autofocus request success");
} else {
errorlog("failed to send autofocus 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;
updateVideoTransform(session.rpcs[UUID].videoElement);
}
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 !== null || session.rpcs[UUID].flipState !== null) {
applyMirrorGuest(
!!session.rpcs[UUID].mirrorState,
session.rpcs[UUID].videoElement,
session.rpcs[UUID].flipState !== null ? !!session.rpcs[UUID].flipState : undefined
);
}
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("legal").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 if (!session.noScreenShare) {
mediaPool.push(session.screenShareElement);
} else {
session.screenShareElement.style.display = "none";
}
}
}
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;
var isScreenShareFeed = false;
try {
if ("realUUID" in session.rpcs[i]) {
isScreenShareFeed = true;
} else if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.dataset && session.rpcs[i].videoElement.dataset.sid) {
isScreenShareFeed = session.rpcs[i].videoElement.dataset.sid.endsWith(":s");
}
} catch (e) { }
if (session.noScreenShare && isScreenShareFeed) {
doNotPush = true;
if (session.rpcs[i].videoElement) {
session.rpcs[i].videoElement.style.display = "none";
}
}
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 => {
log("remove track3");
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) {
const rpc = session.rpcs[mediaPool[NW - 1].dataset.UUID];
const currentUUID = mediaPool[NW - 1].dataset.UUID;
// Skip parent UUID if a _screen counterpart exists (avoid double-counting)
if (!currentUUID.endsWith("_screen") && session.rpcs[currentUUID + "_screen"]) {
// This is a parent with a _screen entry; let the _screen entry be counted instead
continue;
}
// Check for screen share indicators: flag, UUID suffix, or stream ID suffix
const isScreen = rpc.screenShareState ||
currentUUID.endsWith("_screen") ||
(mediaPool[NW - 1].dataset.sid && mediaPool[NW - 1].dataset.sid.endsWith(":s"));
if (isScreen && !rpc.smallScreen) {
let hasLiveScreenVideo = false;
try {
// Check streamSrc first (most reliable for local/direct)
if (rpc.streamSrc && rpc.streamSrc.getVideoTracks) {
hasLiveScreenVideo = rpc.streamSrc.getVideoTracks().some(trk => trk.readyState === "live");
}
// Fallback: check the video element's srcObject if streamSrc is missing/empty
if (!hasLiveScreenVideo && mediaPool[NW - 1].srcObject && mediaPool[NW - 1].srcObject.getVideoTracks) {
hasLiveScreenVideo = mediaPool[NW - 1].srcObject.getVideoTracks().some(trk => trk.readyState === "live");
}
} catch (e) { }
console.log("updateMixer check:", {
uuid: mediaPool[NW - 1].dataset.UUID,
isScreen,
hasLiveScreenVideo,
screenShareState: rpc.screenShareState
});
if (hasLiveScreenVideo) {
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;
var allowScreenshareAutoLayout = !session.layout || session.activeSpeaker || session.alignRight;
var effectiveScreenshareStyle = session.screenshareStyle;
if (!effectiveScreenshareStyle && session.alignRight) {
effectiveScreenshareStyle = 2;
}
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 && effectiveScreenshareStyle && allowScreenshareAutoLayout) {
customLayout = {};
let spotlightLayout;
if (mediaPool.length >= 12) {
spotlightLayout = { x: 10, y: 10, w: 90, h: 90, c: false };
} else if (mediaPool.length >= 10) {
spotlightLayout = { x: 0, y: 10, w: 100, h: 90, c: false };
} else if (mediaPool.length >= 8) {
spotlightLayout = { x: 20, y: 20, w: 80, h: 80, c: false };
} else if (mediaPool.length == 7) {
spotlightLayout = { x: 16.66667, y: 0, w: 83.33333, h: 100, c: false };
} else if (mediaPool.length == 5 || mediaPool.length == 6) {
spotlightLayout = { x: 20, y: 0, w: 80, h: 100, c: false };
} else {
spotlightLayout = { x: 20, y: 0, w: 80, h: 100, c: false };
}
if (effectiveScreenshareStyle === 2) {
if (spotlightLayout.x < 20) {
spotlightLayout.x = 20;
}
if (spotlightLayout.w > 80) {
spotlightLayout.w = 80;
}
}
customLayout[sssid] = spotlightLayout;
if (effectiveScreenshareStyle === 2 && mediaPool.length > 6) {
var columnSlot = 0;
var totalOthers = mediaPool.length - 1;
var slotHeight = totalOthers > 0 ? 100 / totalOthers : 100;
for (var i = 0; i < mediaPool.length; i++) {
if (mediaPool[i].dataset.sid === sssid) {
continue;
}
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: slotHeight * columnSlot, w: 20, h: slotHeight, c: true };
columnSlot += 1;
}
} else {
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 && allowScreenshareAutoLayout) {
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 if (mediaPool.length == 1) {
customLayout[sssid] = { x: 0, y: 0, w: 100, 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 = '';
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 += '';
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;
var vidUUID = vid.dataset.UUID;
var vidSid = vid.dataset.sid;
if (vidUUID && session.rpcs[vidUUID] && !vidSid && session.rpcs[vidUUID].streamID) {
vidSid = session.rpcs[vidUUID].streamID;
}
if (vid.id === "screensharesource" || (vidUUID && vidUUID.endsWith("_screen")) || (vidSid && vidSid.endsWith(":s"))) {
isScreenShare = true;
} else if (vidUUID && session.rpcs[vidUUID] && session.rpcs[vidUUID].screenShareState) {
if (!session.rpcs[vidUUID + "_screen"]) {
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.dataset) {
vid.dataset.rotated = vid.rotated ? vid.rotated : "0";
}
updateVideoTransform(vid);
} else if (vid.dataset && vid.dataset.rotated) {
vid.dataset.rotated = "0";
updateVideoTransform(vid);
}
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 => `${escapeHtml(String(item.value))}`)
.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 = '';
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 = "";
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 = "";
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 = "";
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);
}
}
function showInviteQR() {
var inviteURL = getById("inviteLinkURL").href;
warnUser("Loading QR Code...");
loadQR(function(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);
}, inviteURL);
}
if (typeof session.pendingFramegrabAudioSettings === "undefined") {
session.pendingFramegrabAudioSettings = null;
}
if (typeof session.framegrabAudioPending === "undefined") {
session.framegrabAudioPending = false;
}
if (typeof session.framegrabAudioRetryTimer === "undefined") {
session.framegrabAudioRetryTimer = null;
}
if (typeof session.framegrabAudioRetryAttempts === "undefined") {
session.framegrabAudioRetryAttempts = 0;
}
if (typeof session.pendingMicRefreshTimeout === "undefined") {
session.pendingMicRefreshTimeout = null;
}
session.updateFramegrabAudioUI = function (enable) {
try {
const controls = getById("controlButtons");
const micButton = getById("mutebutton");
const speakerButton = getById("mutespeakerbutton");
if (enable) {
if (controls) {
controls.classList.remove("hidden");
}
if (micButton) {
micButton.classList.remove("hidden");
micButton.style.removeProperty("display");
}
if (speakerButton) {
speakerButton.classList.remove("hidden");
speakerButton.style.removeProperty("display");
}
} else if (speakerButton) {
speakerButton.classList.add("hidden");
}
} catch (err) {
errorlog(err);
}
};
session.clearFramegrabAudioTracks = function (stopTracks = true) {
const removeTracks = stream => {
if (!stream) {
return;
}
try {
stream.getAudioTracks().forEach(track => {
try {
stream.removeTrack(track);
} catch (err) { }
if (stopTracks) {
try {
track.stop();
} catch (stopErr) { }
}
});
} catch (err) { }
};
removeTracks(session.streamSrc);
removeTracks(session.streamSrcClone);
if (session.videoElement && session.videoElement.srcObject) {
removeTracks(session.videoElement.srcObject);
}
session.framegrabAudioInitialized = false;
session.framegrabAudioAutoSelectionApplied = false;
session.framegrabAudioEnabling = false;
session.framegrabAudioPending = false;
session.framegrabAudioRetryAttempts = 0;
if (session.framegrabAudioRetryTimer) {
clearTimeout(session.framegrabAudioRetryTimer);
session.framegrabAudioRetryTimer = null;
}
};
session.prepareFramegrabAudioPreference = function (settings = {}) {
if (!settings) {
return;
}
if (Object.prototype.hasOwnProperty.call(settings, "deviceId")) {
const deviceId = settings.deviceId;
if (deviceId === null || deviceId === false || deviceId === "") {
session.audioDevice = 0;
} else if (deviceId === 1 || deviceId === "1" || deviceId === "default") {
session.audioDevice = 1;
} else if (deviceId === "communications") {
session.audioDevice = "communications";
} else if (Array.isArray(deviceId)) {
session.audioDevice = deviceId.filter(Boolean);
} else {
session.audioDevice = String(deviceId);
}
} else if (!session.audioDevice || session.audioDevice === 0) {
session.audioDevice = 1;
}
};
session.autoSelectFramegrabAudioDevice = function () {
if (!session.framegrabAudio) {
return false;
}
const audioMenu = getById("audioSource3");
if (!audioMenu) {
return false;
}
const inputs = Array.from(audioMenu.querySelectorAll("input[type='checkbox']"));
if (!inputs.length) {
return false;
}
const hasActiveMic = inputs.some(input => input.id !== "multiselect1" && input.checked);
if (hasActiveMic) {
session.framegrabAudioAutoSelectionApplied = true;
return true;
}
const noAudioOption = inputs.find(input => input.id === "multiselect1");
const findByValue = value => inputs.find(input => input.value === value);
let candidate = null;
const preferList = [];
if (Array.isArray(session.audioDevice) && session.audioDevice.length) {
preferList.push(...session.audioDevice);
} else if (session.audioDevice === 0) {
return false;
} else if (session.audioDevice === 1 || session.audioDevice === "1") {
preferList.push("default", "communications");
} else if (session.audioDevice) {
preferList.push(session.audioDevice);
}
for (let i = 0; i < preferList.length; i++) {
const value = preferList[i];
if (typeof value !== "string") {
continue;
}
const directMatch = findByValue(value);
if (directMatch) {
candidate = directMatch;
break;
}
const desired = normalizeDeviceLabel(value);
const labelMatch = inputs.find(input => {
const label = input.dataset && input.dataset.label ? input.dataset.label : (input.getAttribute ? input.getAttribute("data-label") : "");
return label && normalizeDeviceLabel(label) === desired;
});
if (labelMatch) {
candidate = labelMatch;
break;
}
}
if (!candidate) {
candidate = findByValue("default") || findByValue("communications");
}
if (!candidate) {
candidate = inputs.find(input => input.id !== "multiselect1");
}
if (!candidate) {
return false;
}
try {
candidate.checked = true;
candidate.dispatchEvent(new Event("change", { bubbles: true }));
} catch (err) { }
if (typeof SelectedAudioInputDevices !== "undefined") {
if (!Array.isArray(SelectedAudioInputDevices)) {
SelectedAudioInputDevices = [];
}
SelectedAudioInputDevices = SelectedAudioInputDevices.filter(value => value && value !== "ZZZ");
if (!SelectedAudioInputDevices.includes(candidate.value)) {
SelectedAudioInputDevices.push(candidate.value);
}
}
if (noAudioOption) {
noAudioOption.checked = false;
}
session.framegrabAudioAutoSelectionApplied = true;
return true;
};
session.applyFramegrabAudioSettings = async function (settings = {}) {
const enable = !(settings && settings.enable === false);
const clearRetry = () => {
if (session.framegrabAudioRetryTimer) {
clearTimeout(session.framegrabAudioRetryTimer);
session.framegrabAudioRetryTimer = null;
}
session.framegrabAudioRetryAttempts = 0;
};
const scheduleRetry = () => {
if (!session.framegrabAudio || !session.framegrabAudioRequested) {
return;
}
session.pendingFramegrabAudioSettings = settings || { enable: true };
session.framegrabAudioPending = true;
const MAX_RETRIES = 6;
const RETRY_DELAY_MS = 600;
if (session.framegrabAudioRetryAttempts >= MAX_RETRIES) {
log('[FRAMEGRAB AUDIO] Retry limit reached; disabling audio');
session.framegrabAudio = false;
session.framegrabAudioRequested = false;
session.pendingFramegrabAudioSettings = null;
session.updateFramegrabAudioUI(false);
session.clearFramegrabAudioTracks();
session.framegrabAudioPending = false;
if (!session.cleanOutput) {
warnUser('Unable to attach an audio input for the framegrab. Please confirm microphone access and try again.', 6000);
}
return;
}
session.framegrabAudioRetryAttempts += 1;
if (session.framegrabAudioRetryTimer) {
return;
}
session.framegrabAudioRetryTimer = setTimeout(() => {
session.framegrabAudioRetryTimer = null;
if (!session.framegrabAudio || !session.framegrabAudioRequested) {
return;
}
session.applyFramegrabAudioSettings(session.pendingFramegrabAudioSettings || { enable: true }).catch(errorlog);
}, RETRY_DELAY_MS);
};
if (!enable) {
session.pendingFramegrabAudioSettings = null;
session.framegrabAudio = false;
session.framegrabAudioRequested = false;
session.framegrabAudioPending = false;
session.updateFramegrabAudioUI(false);
clearRetry();
session.clearFramegrabAudioTracks();
return;
}
session.framegrabAudio = true;
session.framegrabAudioRequested = true;
session.framegrabAudioPending = false;
session.pendingFramegrabAudioSettings = settings || { enable: true };
session.updateFramegrabAudioUI(true);
session.framegrabAudioAutoSelectionApplied = false;
session.prepareFramegrabAudioPreference(settings || {});
const overrideConstraints = (() => {
if (!settings || typeof settings !== "object") {
return false;
}
if (!Object.prototype.hasOwnProperty.call(settings, "deviceId")) {
return false;
}
let desiredId = settings.deviceId;
if (Array.isArray(desiredId)) {
desiredId = desiredId.find(Boolean) || null;
}
if (
desiredId === null ||
desiredId === false ||
desiredId === "" ||
desiredId === 0 ||
desiredId === "0" ||
desiredId === 1 ||
desiredId === "1" ||
desiredId === "default" ||
desiredId === "communications"
) {
return false;
}
desiredId = String(desiredId);
return { audio: { deviceId: desiredId } };
})();
if (!session.streamSrc) {
session.framegrabAudioPending = true;
return;
}
let deviceInfos = null;
try {
deviceInfos = await enumerateDevices();
gotDevices(deviceInfos);
} catch (err) {
errorlog(err);
}
const audioInputsAvailable = Array.isArray(deviceInfos) && deviceInfos.some(info => info && info.kind === 'audioinput');
let autoSelectOk = false;
try {
autoSelectOk = session.autoSelectFramegrabAudioDevice() === true;
} catch (err) {
errorlog(err);
}
if (!autoSelectOk) {
if (!audioInputsAvailable) {
log('[FRAMEGRAB AUDIO] No audio inputs detected; retrying');
}
scheduleRetry();
return;
}
try {
await grabAudio("#audioSource3", null, overrideConstraints);
session.framegrabAudioInitialized = true;
const trackCount = session.streamSrc && typeof session.streamSrc.getAudioTracks === "function"
? session.streamSrc.getAudioTracks().length
: 0;
if (!trackCount) {
session.framegrabAudioPending = true;
scheduleRetry();
return;
}
session.framegrabAudioPending = false;
if (trackCount) {
try {
session.seedStream();
} catch (err) {
errorlog(err);
}
}
clearRetry();
session.pendingFramegrabAudioSettings = null;
} catch (err) {
errorlog(err);
scheduleRetry();
}
};
session.startFramegrabAudio = async function (overrideSettings = null) {
if (!session.framegrab) {
return false;
}
const pendingSettings = overrideSettings || session.pendingFramegrabAudioSettings || { enable: true };
if (pendingSettings && pendingSettings.enable === false) {
return session.applyFramegrabAudioSettings(pendingSettings);
}
session.framegrabAudioRequested = true;
session.framegrabAudio = true;
const settings = Object.assign({ enable: true }, pendingSettings || {});
session.pendingFramegrabAudioSettings = settings;
session.updateFramegrabAudioUI(true);
if (!session.streamSrc) {
session.framegrabAudioPending = true;
return true;
}
if (session.framegrabAudioEnabling) {
return session.streamSrc.getAudioTracks && session.streamSrc.getAudioTracks().length > 0;
}
session.framegrabAudioEnabling = true;
activatedPreview = false;
let success = false;
try {
await session.applyFramegrabAudioSettings(settings);
const hasAudio = session.streamSrc && session.streamSrc.getAudioTracks && session.streamSrc.getAudioTracks().length > 0;
if (hasAudio) {
session.muted = false;
const muteToggle = getById("mutetoggle");
if (muteToggle) {
muteToggle.className = "las la-microphone toggleSize";
}
const muteButton = getById("mutebutton");
if (muteButton) {
muteButton.classList.remove("red", "pulsate");
muteButton.ariaPressed = "false";
}
if (!session.cleanOutput) {
try {
getById("header").classList.remove("red");
} catch (err) { }
}
success = true;
}
} catch (err) {
errorlog(err);
warnUser("Unable to access the microphone. Please check browser permissions.", 6000);
session.framegrabAudio = false;
session.framegrabAudioRequested = false;
session.updateFramegrabAudioUI(false);
} finally {
session.framegrabAudioEnabling = false;
activatedPreview = false;
}
const result = success || session.framegrabAudioPending;
if (!result) {
session.framegrabAudio = false;
session.framegrabAudioRequested = false;
session.updateFramegrabAudioUI(false);
}
return result;
};
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 === "framegrab-audio-settings") {
if (!session.framegrab || typeof session.startFramegrabAudio !== "function" || typeof session.applyFramegrabAudioSettings !== "function") {
return;
}
const enable = !(typeof e.data.enable !== "undefined" && e.data.enable === false);
const payload = { enable };
if (enable && typeof e.data.deviceId !== "undefined" && e.data.deviceId !== null) {
payload.deviceId = e.data.deviceId;
}
try {
const handler = enable ? session.startFramegrabAudio : session.applyFramegrabAudioSettings;
const maybePromise = handler(payload);
if (maybePromise && typeof maybePromise.then === "function") {
maybePromise.catch(errorlog);
}
} catch (err) {
errorlog(err);
}
} 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, session.rpcs[UUID].flipState);
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, session.rpcs[UUID].flipState);
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;
}
sessionStorage.setItem("jvi", "1"); // joined via input - for showing invite link header
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;
}
return session.sendMessage(msg, UUID);
}
} catch (e) {
errorlog(e);
}
return false;
}
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 = '';
insert.appendChild(muteInsert);
} else if (session.rpcs[UUID].voiceMeter) {
insert.appendChild(session.rpcs[UUID].voiceMeter);
}
//getById("userList").innerHTML += " ";
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) {
activelyProcessingDraw = false;
return;
}
// Use videoWidth/videoHeight for actual current dimensions
const srcWidth = session.canvasSource.videoWidth || session.canvasSource.width;
const srcHeight = session.canvasSource.videoHeight || session.canvasSource.height;
if (!srcWidth) {
activelyProcessingDraw = false;
return;
}
session.canvas.height = 2 * parseInt(srcHeight / 2);
session.canvas.width = 2 * parseInt(srcWidth / 2);
if (session.effectValue) {
// Smooth out the zoom factor
zz = 0.9 * zz + session.effectValue * 0.1;
// Calculate the scaled dimensions
const scaledWidth = srcWidth / zz;
const scaledHeight = srcHeight / zz;
// Calculate the offset based on position sliders
// This centers the zoom on the selected position
xa = (srcWidth - scaledWidth) * xPosition;
ya = (srcHeight - scaledHeight) * yPosition;
// Draw the zoomed region
session.canvasCtx.drawImage(
session.canvasSource,
xa, ya,
scaledWidth,
scaledHeight,
0, 0,
srcWidth,
srcHeight
);
} else {
// If no zoom, draw the full image
session.canvasCtx.drawImage(
session.canvasSource,
0, 0,
srcWidth,
srcHeight,
0, 0,
srcWidth,
srcHeight
);
}
} 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) {
var rpc = session.rpcs && session.rpcs[UUID] ? session.rpcs[UUID] : null;
if (isIFrame && rpc) {
parent.postMessage({ remoteStats: msg.remoteStats, streamID: rpc.streamID, UUID: UUID }, session.iframetarget);
}
if (!rpc) {
return;
}
var allowUI = rpc.allowGraphs || session.allowGraphs;
if (allowUI && 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");
} else {
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;
}
// Store URL for QoS hostname extraction (may not exist in all browsers)
if ("url" in candidate && candidate.url) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_url = candidate.url;
} else {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_url;
}
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_relay_url;
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;
}
}
}
// QoS data accumulation
if (session.qosEnabled && session.qosData && !session.qosData.sent) {
try {
var qd = session.qosData;
var peerStats = session.rpcs[UUID].stats["Peer-to-Peer_Connection"];
// Accumulate RTT samples
if (peerStats && peerStats.Round_Trip_Time_ms) {
qd.rttSamples.push(peerStats.Round_Trip_Time_ms);
if (qd.rttSamples.length > 500) qd.rttSamples.shift();
}
// Track transport type and TURN servers
if (peerStats && peerStats.candidateType_local) {
if (peerStats.candidateType_local === "relay") {
qd.transportType = "turn";
// Use URL-based hostname instead of IP (graceful degradation if unavailable)
if (peerStats.local_relay_url && session.qosTurnAllowlist && session.qosTurnAllowlist.length) {
var host = extractTurnHostnameFromUrl(peerStats.local_relay_url);
if (host && session.qosTurnAllowlist.includes(host) && !qd.turnServersUsed.includes(host)) {
qd.turnServersUsed.push(host);
}
}
// Safari/Firefox may not have url - we still get transportType="turn"
} else if (!qd.transportType) {
qd.transportType = "p2p";
}
if (!qd.candidateTypesLocal.includes(peerStats.candidateType_local)) {
qd.candidateTypesLocal.push(peerStats.candidateType_local);
}
}
if (peerStats && peerStats.candidateType_remote && !qd.candidateTypesRemote.includes(peerStats.candidateType_remote)) {
qd.candidateTypesRemote.push(peerStats.candidateType_remote);
}
// Accumulate packet loss and jitter from tracks
for (var tid in session.rpcs[UUID].stats) {
var trackStat = session.rpcs[UUID].stats[tid];
if (trackStat && typeof trackStat === "object") {
if (trackStat._type === "video") {
if (trackStat.packetLoss_in_percentage !== undefined) {
qd.packetLossVideoSamples.push(trackStat.packetLoss_in_percentage);
if (qd.packetLossVideoSamples.length > 500) qd.packetLossVideoSamples.shift();
}
if (trackStat.Bitrate_in_kbps) {
qd.bitrateSamples.push(trackStat.Bitrate_in_kbps);
if (qd.bitrateSamples.length > 500) qd.bitrateSamples.shift();
}
if (trackStat.Jitter_Buffer_ms) {
qd.jitterSamples.push(trackStat.Jitter_Buffer_ms);
if (qd.jitterSamples.length > 500) qd.jitterSamples.shift();
}
if (trackStat.codec) qd.lastVideoCodec = trackStat.codec;
if (trackStat.Resolution) qd.lastResolution = trackStat.Resolution;
} else if (trackStat._type === "audio") {
if (trackStat.packetLoss_in_percentage !== undefined) {
qd.packetLossAudioSamples.push(trackStat.packetLoss_in_percentage);
if (qd.packetLossAudioSamples.length > 500) qd.packetLossAudioSamples.shift();
}
if (trackStat.codec) qd.lastAudioCodec = trackStat.codec;
}
}
}
} catch (e) { warnlog("QoS accumulation error: " + e); }
}
playoutdelay(UUID);
setTimeout(function () {
session.directorSpeakerMute();
session.directorDisplayMute();
}, 0);
});
}
} catch (e) {
errorlog(e);
}
pokeIframeAPI("view-stats-updated", true, UUID);
}
// QoS stats collection for publisher outbound connections (session.pcs)
// This runs periodically when QoS is enabled to gather stats from publisher connections
function processPcsQosStats(UUID) {
if (!session.qosEnabled || !session.qosData || session.qosData.sent) return;
if (!session.pcs || !(UUID in session.pcs)) return;
try {
session.pcs[UUID].getStats().then(function(stats) {
if (!(UUID in session.pcs)) return;
if (!session.qosEnabled || !session.qosData || session.qosData.sent) return;
var qd = session.qosData;
var nominatedCandidate = null;
var candidates = {};
stats.forEach(function(stat) {
try {
if (stat.id && stat.id.startsWith("DEPRECATED_")) return;
if (stat.type === "outbound-rtp") {
if (stat.kind === "video") {
if (stat.qualityLimitationReason) {
session.pcs[UUID].stats.quality_limitation_reason = stat.qualityLimitationReason;
}
if (stat.frameWidth && stat.frameHeight) {
var res = stat.frameWidth + " x " + stat.frameHeight;
if (stat.framesPerSecond) {
res += " @ " + stat.framesPerSecond;
}
session.pcs[UUID].stats.resolution = res;
if (!qd.lastResolution) qd.lastResolution = res;
}
if (stat.encoderImplementation) {
session.pcs[UUID].stats.encoder = stat.encoderImplementation;
if (!qd.lastVideoCodec) qd.lastVideoCodec = stat.encoderImplementation;
}
// Track bitrate for publishers
if (stat.bytesSent !== undefined && stat.timestamp) {
if (session.pcs[UUID].stats._lastBytesSent !== undefined) {
var timeDiff = stat.timestamp - session.pcs[UUID].stats._lastTimestamp;
if (timeDiff > 0) {
var bitrate = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._lastBytesSent)) / timeDiff);
if (bitrate > 0) {
qd.bitrateSamples.push(bitrate);
if (qd.bitrateSamples.length > 500) qd.bitrateSamples.shift();
}
}
}
session.pcs[UUID].stats._lastBytesSent = stat.bytesSent;
session.pcs[UUID].stats._lastTimestamp = stat.timestamp;
}
}
} else if (stat.type === "remote-candidate") {
candidates[stat.id] = stat;
if (stat.relayProtocol && stat.ip) {
session.pcs[UUID].stats.remote_relay_IP = stat.ip;
session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol;
}
if (stat.candidateType) {
session.pcs[UUID].stats.candidateType_remote = stat.candidateType;
}
} else if (stat.type === "local-candidate") {
candidates[stat.id] = stat;
if (stat.relayProtocol && stat.ip) {
session.pcs[UUID].stats.local_relayIP = stat.ip;
session.pcs[UUID].stats.local_relayProtocol = stat.relayProtocol;
}
if (stat.candidateType) {
session.pcs[UUID].stats.candidateType_local = stat.candidateType;
}
} else if (stat.type === "candidate-pair" && stat.nominated) {
if (!nominatedCandidate || nominatedCandidate.priority < stat.priority) {
nominatedCandidate = stat;
}
} else if (stat.type === "remote-inbound-rtp") {
// Packet loss from remote peer
if (stat.packetsLost !== undefined && stat.packetsReceived !== undefined) {
var total = stat.packetsLost + stat.packetsReceived;
if (total > 0) {
var lossPercent = (stat.packetsLost / total) * 100;
if (stat.kind === "video") {
qd.packetLossVideoSamples.push(lossPercent);
if (qd.packetLossVideoSamples.length > 500) qd.packetLossVideoSamples.shift();
} else if (stat.kind === "audio") {
qd.packetLossAudioSamples.push(lossPercent);
if (qd.packetLossAudioSamples.length > 500) qd.packetLossAudioSamples.shift();
}
}
}
// Jitter from remote peer
if (stat.jitter !== undefined) {
var jitterMs = stat.jitter * 1000;
qd.jitterSamples.push(jitterMs);
if (qd.jitterSamples.length > 500) qd.jitterSamples.shift();
}
}
} catch (e) { }
});
// Process nominated candidate for RTT and transport type
if (nominatedCandidate) {
// RTT
if (nominatedCandidate.totalRoundTripTime && nominatedCandidate.responsesReceived) {
var rtt = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000);
session.pcs[UUID].stats.average_roundTripTime_ms = rtt;
qd.rttSamples.push(rtt);
if (qd.rttSamples.length > 500) qd.rttSamples.shift();
}
if (nominatedCandidate.currentRoundTripTime) {
var currentRtt = parseInt(nominatedCandidate.currentRoundTripTime * 1000);
qd.rttSamples.push(currentRtt);
if (qd.rttSamples.length > 500) qd.rttSamples.shift();
}
// Transport type from nominated candidate
var localCandidate = candidates[nominatedCandidate.localCandidateId];
var remoteCandidate = candidates[nominatedCandidate.remoteCandidateId];
if (localCandidate) {
if (localCandidate.candidateType === "relay") {
qd.transportType = "turn";
// Use stat.url to get TURN hostname (official servers only)
// Note: stat.url may not be available in all browsers (graceful degradation)
if (localCandidate.url && session.qosTurnAllowlist && session.qosTurnAllowlist.length) {
var host = extractTurnHostnameFromUrl(localCandidate.url);
if (host && session.qosTurnAllowlist.includes(host) && !qd.turnServersUsed.includes(host)) {
qd.turnServersUsed.push(host);
}
}
// If url unavailable (Safari/Firefox), we still track transportType="turn"
// but skip hostname - better no data than wrong/private data
} else if (!qd.transportType || qd.transportType === "unknown") {
qd.transportType = "p2p";
}
if (!qd.candidateTypesLocal.includes(localCandidate.candidateType)) {
qd.candidateTypesLocal.push(localCandidate.candidateType);
}
}
if (remoteCandidate && !qd.candidateTypesRemote.includes(remoteCandidate.candidateType)) {
qd.candidateTypesRemote.push(remoteCandidate.candidateType);
}
}
// Schedule next stats collection (every 5 seconds)
if (UUID in session.pcs && session.qosEnabled && session.qosData && !session.qosData.sent) {
clearTimeout(session.pcs[UUID].qosStatsTimeout);
session.pcs[UUID].qosStatsTimeout = setTimeout(processPcsQosStats, 5000, UUID);
}
}).catch(function(e) {
warnlog("QoS pcs stats error: " + e);
});
} catch (e) {
warnlog("QoS pcs stats error: " + e);
}
}
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 = "
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: " + streamID + " ";
if (statsObj.chunked_mode_video && typeof statsObj.chunked_mode_video.buffer_buffer !== "undefined") {
var chunkVideo = statsObj.chunked_mode_video;
var chunkSummary = "Video Buffer: " + parseInt(chunkVideo.buffer_buffer || 0) + " ms / Δ " + parseInt(chunkVideo.buffer_delta || 0) + " ms";
if (chunkVideo.rebuffering) {
chunkSummary += " (rebuffering)";
}
menu.innerHTML += chunkSummary + " ";
if (typeof chunkVideo.fec_repairs !== "undefined" || typeof chunkVideo.nacks_sent !== "undefined") {
var repairSummary = [];
if (typeof chunkVideo.fec_repairs !== "undefined") {
repairSummary.push("FEC " + parseInt(chunkVideo.fec_repairs || 0));
}
if (typeof chunkVideo.nacks_sent !== "undefined") {
repairSummary.push("NACK " + parseInt(chunkVideo.nacks_sent || 0));
}
if (repairSummary.length) {
menu.innerHTML += "Video Repairs: " + repairSummary.join(" / ") + " ";
}
}
}
if (statsObj.chunked_mode_audio && typeof statsObj.chunked_mode_audio.buffer_buffer !== "undefined") {
var chunkAudio = statsObj.chunked_mode_audio;
var audioSummary = "Audio Buffer: " + parseInt(chunkAudio.buffer_buffer || 0) + " ms / Δ " + parseInt(chunkAudio.buffer_delta || 0) + " ms";
if (chunkAudio.rebuffering) {
audioSummary += " (rebuffering)";
}
menu.innerHTML += audioSummary + " ";
}
//// 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 += "";
//}
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 += `
${tmp}
`;
if (key == "info") {
out += printValues(obj[key]);
} else if (key == "meta") {
out += `
`;
Object.entries(obj[key]).forEach(([category, data]) => {
if (data.type === "file" && data.filetype && data.filetype.startsWith("image/")) {
out += `
`;
} 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 = "" + value + "";
}
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 += "";
}
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 = "" + value + "";
} else if (key == "remote_relay_IP") {
value = "" + value + "";
} 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 (Stereo-mode)";
if (value == 3) {
value = "3 (outbound hi-fi) Use Headphones";
} else if (value == 1) {
value = "1 (in & out hi-fi) Use Headphones";
} else if (value == 2) {
value = "3 (inbound hi-fi)";
} else if (value == 4) {
value = "3 (multichannel) Use Headphones";
} else if (value == 5) {
value = "5 (auto-mode) Use Headphones";
}
} 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 +=
'
';
out += 'Open Location in Google Maps';
}
}
stat = stat.replaceAll("_", " ");
stat = stat.trim();
if (hint) {
out += "
" + stat + "" + value + unit + "
";
} else {
out += "
" + stat + "" + value + unit + "
";
}
} 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 += "";
}
});
if (session.promptAccess) {
if (UUID && session.pcs[UUID]) {
menu.innerHTML += "";
}
}
if (UUID && session.pcs[UUID] && session.pcs[UUID].restartIce) {
// only show if available
menu.innerHTML += "";
}
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 = "" + value + "";
}
if (key == "local_relay_IP") {
value = "" + value + "";
}
if (key == "remote_relay_IP") {
value = "" + value + "";
}
if (key == "watch_URL") {
value = "" + value + "";
}
if (key == "candidateType_local" && value == "relay") {
stat = "Candidate type - Local";
value = "💸
relay server
";
} else if (key == "candidateType_remote" && value == "relay") {
stat = "Candidate type - Remote";
value = "💸
relay server
";
} else if (key == "candidateType_local" && value == "host") {
stat = "Candidate type - Local";
value = "
host
";
} else if (key == "candidateType_remote" && value == "host") {
stat = "Candidate type - Remote";
value = "
host
";
} else if (key == "candidateType_local" && value == "srflx") {
stat = "Candidate type - Local";
value = "
srflx
";
} else if (key == "candidateType_remote" && value == "srflx") {
stat = "Candidate type - Remote";
value = "
srflx
";
} 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 = "" + value + "";
}
stat = stat.replaceAll("_", " ");
if (hint) {
menu.innerHTML += "
" + stat + "" + value + unit + "
";
} else {
menu.innerHTML += "
" + stat + "" + value + unit + "
";
}
}
});
if (UUID && session.pcs[UUID]) {
if (session.pcs[UUID].maxBandwidth) {
menu.innerHTML += "
max bandwidth target" + session.pcs[UUID].maxBandwidth + "
";
}
if (session.pcs[UUID].setBitrate) {
menu.innerHTML += '
Viewers need &showtips in their URL to see tip buttons.
' +
'
4. QR Code Feature
' +
'
A scannable QR code will appear on your video periodically. Use ¬ipqr to disable, or &tipqrsize=200 to resize.
' +
'
' +
'' +
'' +
'
' +
'
' +
'
' +
'';
document.body.insertAdjacentHTML("beforeend", modalHTML);
}
function closeTipOnboarding(permanent) {
if (permanent) {
setStorage("tipOnboardingSeen", "true", 9999); // Never show again
} else {
setStorage("tipOnboardingSeen", "true", 1); // Show again in 1 day
}
var modal = document.getElementById("tipOnboardingModal");
var backdrop = document.getElementById("tipOnboardingBackdrop");
if (modal) modal.remove();
if (backdrop) backdrop.remove();
}
async function applyTipUsername() {
var input = document.getElementById("tipUsernameInput");
if (!input) return;
var username = input.value.trim().toLowerCase();
if (!username) {
warnUser("Please enter a username");
return;
}
// Validate username format (alphanumeric, underscore, hyphen, 3-30 chars)
if (!/^[a-z0-9_-]{3,30}$/.test(username)) {
warnUser("Username must be 3-30 characters (letters, numbers, _ or -)");
return;
}
// Fetch performer info from API to get overlay token
var tipServer = session.tipServer || "https://ninjabacker.com";
try {
var response = await fetch(tipServer + "/v1/performer/" + username);
if (!response.ok) {
warnUser("Username not found or not registered for tips");
return;
}
var data = await response.json();
if (!data.overlay_token) {
warnUser("Performer account not fully set up");
return;
}
// Build new URL with tipsid parameter (overlay token)
var url = new URL(window.location.href);
url.searchParams.delete("tip");
url.searchParams.delete("tips");
url.searchParams.delete("tipid");
url.searchParams.set("tipsid", data.overlay_token);
// Reload with new parameter
window.location.href = url.toString();
} catch(e) {
errorlog("Failed to lookup performer:", e);
warnUser("Failed to verify username. Please try again.");
}
}
// Get currency symbol helper
function getTipCurrencySymbol(currency) {
var symbols = { USD: "$", EUR: "\u20AC", GBP: "\u00A3", CAD: "C$", AUD: "A$", JPY: "\u00A5" };
return symbols[currency] || currency + " ";
}
// Show on-screen tip banner (performer only)
function showTipBanner(tipData) {
var currencySymbol = getTipCurrencySymbol(tipData.currency || "USD");
var fromLabel = sanitizeLabel(tipData.fromLabel || tipData.from || "Anonymous");
var amount = tipData.amount;
// Create banner element
var banner = document.createElement("div");
banner.className = "tipBanner";
banner.innerHTML = currencySymbol + amount + " tip from " + fromLabel;
if (tipData.message) {
banner.innerHTML += '
"' + sanitizeChat(tipData.message) + '"
';
}
document.body.appendChild(banner);
// Trigger animation
setTimeout(function() {
banner.classList.add("tipBannerShow");
}, 10);
// Remove after 5 seconds
setTimeout(function() {
banner.classList.remove("tipBannerShow");
banner.classList.add("tipBannerHide");
setTimeout(function() {
banner.remove();
}, 500); // Wait for fade out animation
}, 5000);
}
// Process incoming tip message
function processTipMessage(tipData, UUID) {
log("Tip received:", tipData);
var currencySymbol = getTipCurrencySymbol(tipData.currency || "USD");
var fromLabel = sanitizeLabel(tipData.fromLabel || tipData.from || "Anonymous");
var message = tipData.message ? sanitizeChat(tipData.message) : "";
// Plain text version for notification
var notifyMsg = currencySymbol + tipData.amount + " tip from " + fromLabel;
if (message) {
notifyMsg += ': "' + message + '"';
}
var data = {
time: Date.now(),
type: "tip",
msg: notifyMsg,
label: "Tip"
};
messageList.push(data);
messageList = messageList.slice(-100);
// Play notification sound
if (session.beepToNotify) {
playtone();
showNotification("Tip received", notifyMsg);
}
updateMessages();
// Show on-screen banner only for SSE-received tips (performer's own tips)
if (UUID === null) {
showTipBanner(tipData);
}
// Chat notification (red dot) when chat is closed
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");
}
// Broadcast to popout chat window
if (session.broadcastChannel !== false) {
session.broadcastChannel.postMessage(data);
}
// Browser notification
if (Notification.permission === "granted") {
try {
new Notification("Tip Received!", {
body: notifyMsg,
icon: "./media/logo.png"
});
} catch(e) {}
}
// API callback
if (typeof pokeAPI === 'function') {
pokeAPI("tip", tipData);
}
// Iframe postMessage
if (isIFrame && session.iframetarget) {
try {
parent.postMessage({ action: "tip", value: tipData }, session.iframetarget);
} catch(e) {}
}
}
// Initialize SSE connection for tip notifications
function initTipNotifications() {
if (!session.receiveTips) return;
if (session.tipEventSource) return; // Already connected
// Use tipsId (overlay token) for SSE subscription, fallback to streamID for legacy
var subscribeId = session.tipsId || session.streamID;
if (!subscribeId) return;
var tipServer = session.tipServer || "https://ninjabacker.com";
var sseURL = tipServer + "/v1/subscribe/" + subscribeId;
try {
session.tipEventSource = new EventSource(sseURL);
session.tipEventSource.onmessage = function(event) {
try {
var tipData = JSON.parse(event.data);
if (tipData.type === "tip") {
processTipMessage(tipData, null);
// Broadcast to room peers
broadcastTipReceived(tipData);
}
} catch(e) {
errorlog("Tip SSE parse error:", e);
}
};
session.tipEventSource.onerror = function(err) {
warnlog("Tip SSE connection error, will retry...");
};
log("Tip notifications initialized for: " + subscribeId);
} catch(e) {
errorlog("Failed to initialize tip notifications:", e);
}
}
// Fetch performer info (username) from tipsId token and set session.tipId
async function fetchPerformerFromToken() {
if (!session.tipsId) return;
if (session.tipId) return; // Already have username
var tipServer = session.tipServer || "https://ninjabacker.com";
try {
var response = await fetch(tipServer + "/v1/performer/" + session.tipsId);
if (response.ok) {
var data = await response.json();
if (data.username) {
session.tipId = data.username;
log("Performer username from token: " + data.username);
}
}
} catch(e) {
errorlog("Failed to fetch performer from token:", e);
}
}
// Close SSE connection
function closeTipNotifications() {
if (session.tipEventSource) {
session.tipEventSource.close();
session.tipEventSource = null;
}
}
// Broadcast tip received to all peers (so viewers see it too)
function broadcastTipReceived(tipData) {
var msg = { tip: tipData };
session.sendPeers(msg);
}
// Open tip modal for a peer
function openTipModal(UUID) {
var peer = session.rpcs[UUID] || session.pcs[UUID];
if (!peer || !peer.acceptsTips) {
warnUser("This user does not accept tips");
return;
}
var peerLabel = sanitizeLabel(peer.tipId || peer.label || peer.streamID || "Performer");
var amounts = peer.tipAmounts || session.tipAmounts || [5, 10, 25, 50, 100];
var currency = peer.tipCurrency || session.tipCurrency || "USD";
var currencySymbol = getTipCurrencySymbol(currency);
var modalID = "tipModal_" + UUID;
var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length;
var amountButtons = amounts.map(function(amt) {
return '';
}).join('');
var modalTemplate =
'
' +
'
' +
'' +
'
' +
'
\uD83D\uDCB0 Send a tip to ' + peerLabel + '
' +
'
' +
'
' + amountButtons + '
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'Powered by' +
'' +
'
' +
'
' +
'Selected: ' + currencySymbol + '0' +
'
' +
'' +
'' +
'
' +
'
';
document.body.insertAdjacentHTML("beforeend", modalTemplate);
// Initialize Stripe Elements
initTipStripeElements(UUID, peer);
}
// Load Stripe.js dynamically if not already loaded
function loadStripeJS() {
return new Promise(function(resolve, reject) {
if (typeof Stripe !== 'undefined') {
resolve();
return;
}
var script = document.createElement('script');
script.src = 'https://js.stripe.com/v3/';
script.onload = resolve;
script.onerror = function() { reject(new Error('Failed to load Stripe.js')); };
document.head.appendChild(script);
});
}
// Initialize Stripe Elements for tip modal
async function initTipStripeElements(UUID, peer) {
peer = peer || {};
var tipServer = peer.tipServer || session.tipServer || "https://ninjabacker.com";
try {
// Load Stripe.js if needed
await loadStripeJS();
// Get performer-specific Stripe publishable key (supports test/live mode per account)
var performerId = peer.tipId || peer.tipsId || peer.streamID;
var stripeKey = null;
if (performerId) {
try {
var perfResponse = await fetch(tipServer + "/v1/performer/" + performerId);
if (perfResponse.ok) {
var perfData = await perfResponse.json();
stripeKey = perfData.stripePublishableKey;
}
} catch(e) {
// Fall back to config endpoint
}
}
// Fallback to global config if performer-specific key not available
if (!stripeKey) {
var response = await fetch(tipServer + "/v1/config");
var config = await response.json();
stripeKey = config.stripePublishableKey;
}
if (!stripeKey) {
document.getElementById("tipError_" + UUID).textContent = "Tipping not configured";
document.getElementById("tipError_" + UUID).classList.remove("hidden");
return;
}
// Initialize Stripe with performer-specific key
var stripe = Stripe(stripeKey);
var elements = stripe.elements();
var cardElement = elements.create('card', {
hidePostalCode: true,
style: {
base: {
color: '#ffffff',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
fontSize: '16px',
'::placeholder': { color: '#a0a0a0' }
}
}
});
cardElement.mount('#tipCardElement_' + UUID);
// Store for later use
if (!window.tipStripeElements) window.tipStripeElements = {};
window.tipStripeElements[UUID] = { stripe: stripe, cardElement: cardElement };
cardElement.on('change', function(event) {
var amount = window.tipStripeElements[UUID].selectedAmount || 0;
document.getElementById("tipConfirmBtn_" + UUID).disabled = !event.complete || amount <= 0;
});
} catch(e) {
errorlog("Failed to init Stripe:", e);
document.getElementById("tipError_" + UUID).textContent = "Failed to load payment form";
document.getElementById("tipError_" + UUID).classList.remove("hidden");
}
}
// Select a predefined tip amount
function selectTipAmount(btn, UUID) {
document.querySelectorAll('#tipModal_' + UUID + ' .tipAmountBtn').forEach(function(b) {
b.classList.remove('selected');
});
btn.classList.add('selected');
var amount = parseFloat(btn.dataset.amount);
var peer = session.rpcs[UUID] || session.pcs[UUID];
var currency = peer?.tipCurrency || session.tipCurrency || "USD";
var currencySymbol = getTipCurrencySymbol(currency);
document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + amount.toFixed(2);
document.getElementById('tipCustomInput_' + UUID).value = "";
if (window.tipStripeElements && window.tipStripeElements[UUID]) {
window.tipStripeElements[UUID].selectedAmount = amount;
}
// Enable button if card is ready
checkTipButtonState(UUID);
}
// Handle custom amount input
function customTipAmount(UUID, value) {
var amount = parseFloat(value);
var peer = session.rpcs[UUID] || session.pcs[UUID];
var currency = peer?.tipCurrency || session.tipCurrency || "USD";
var currencySymbol = getTipCurrencySymbol(currency);
// Deselect preset buttons
document.querySelectorAll('#tipModal_' + UUID + ' .tipAmountBtn').forEach(function(b) {
b.classList.remove('selected');
});
if (amount > 0) {
document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + amount.toFixed(2);
if (window.tipStripeElements && window.tipStripeElements[UUID]) {
window.tipStripeElements[UUID].selectedAmount = amount;
}
} else {
document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + "0";
if (window.tipStripeElements && window.tipStripeElements[UUID]) {
window.tipStripeElements[UUID].selectedAmount = 0;
}
}
checkTipButtonState(UUID);
}
// Check if tip button should be enabled
function checkTipButtonState(UUID) {
if (!window.tipStripeElements || !window.tipStripeElements[UUID]) return;
var amount = window.tipStripeElements[UUID].selectedAmount || 0;
// Button state is also controlled by Stripe card element change event
}
// Confirm and process tip payment
async function confirmTip(UUID) {
if (!window.tipStripeElements || !window.tipStripeElements[UUID]) return;
var stripeData = window.tipStripeElements[UUID];
var peer = session.rpcs[UUID] || session.pcs[UUID];
if (!peer || !stripeData.selectedAmount || stripeData.selectedAmount <= 0) {
return;
}
var tipServer = peer.tipServer || session.tipServer || "https://ninjabacker.com";
var amount = stripeData.selectedAmount;
var currency = peer.tipCurrency || session.tipCurrency || "USD";
var tipperName = document.getElementById('tipName_' + UUID)?.value || "Anonymous";
var message = document.getElementById('tipMessageInput_' + UUID)?.value || "";
var performerUsername = peer.tipId || peer.streamID;
var submitBtn = document.getElementById('tipConfirmBtn_' + UUID);
var errorEl = document.getElementById('tipError_' + UUID);
submitBtn.disabled = true;
submitBtn.textContent = "Processing...";
errorEl.classList.add('hidden');
try {
// Create PaymentIntent
var intentResponse = await fetch(tipServer + "/v1/tip/intent", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: amount,
currency: currency,
performerUsername: performerUsername,
tipperName: tipperName,
message: message
})
});
if (!intentResponse.ok) {
var errData = await intentResponse.json();
throw new Error(errData.error || 'Failed to create payment');
}
var intentData = await intentResponse.json();
// Confirm payment with Stripe
var result = await stripeData.stripe.confirmCardPayment(intentData.clientSecret, {
payment_method: { card: stripeData.cardElement }
});
if (result.error) {
throw new Error(result.error.message);
}
if (result.paymentIntent.status === 'succeeded') {
// Confirm with backend
await fetch(tipServer + "/v1/tip/confirm", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tipId: intentData.tipId,
paymentIntentId: result.paymentIntent.id
})
});
// Success!
closeTipModal('tipModal_' + UUID, UUID);
warnUser("Tip sent successfully! Thank you!", 3000);
}
} catch(e) {
errorlog("Tip payment error:", e);
errorEl.textContent = e.message;
errorEl.classList.remove('hidden');
submitBtn.disabled = false;
submitBtn.textContent = "Send Tip";
}
}
// Close tip modal
function closeTipModal(modalID, UUID) {
var modal = document.getElementById(modalID);
if (modal) modal.remove();
// Cleanup Stripe elements
if (window.tipStripeElements && window.tipStripeElements[UUID]) {
if (window.tipStripeElements[UUID].cardElement) {
window.tipStripeElements[UUID].cardElement.destroy();
}
delete window.tipStripeElements[UUID];
}
}
// =====================
// END TIPPING FUNCTIONALITY
// =====================
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.screensharefps !== false) {
constraints.video.frameRate = {
ideal: session.screensharefps,
max: session.screensharefps
};
} else 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 && !session.cleanViewer) {
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("legal").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) {
// Check if user joined via text input (casual user)
if (sessionStorage.getItem("jvi")) {
sessionStorage.removeItem("jvi");
if (!urlParams.has("push") && !urlParams.has("id") && !urlParams.has("permaid")
&& !urlParams.has("perma") && !urlParams.has("sticky")) {
// Show invite header INSTEAD of "You are in room"
var inviteURL = location.protocol + "//" + location.host + location.pathname + "?room=" + session.roomid;
var urlPW = urlParams.get("password") || urlParams.get("pass") || urlParams.get("pw") || urlParams.get("p");
if (urlPW === "false" || urlPW === "0" || urlPW === "off") {
// Password explicitly disabled
inviteURL += "&password=false";
getById("inviteLinkURL").href = inviteURL;
getById("inviteLinkURL").innerText = inviteURL;
getById("head9").classList.remove("hidden");
} else if (urlPW) {
// Actual password in URL - generate hash
generateHash(session.password + session.salt, 4).then(function(hash) {
inviteURL += "&hash=" + hash;
getById("inviteLinkURL").href = inviteURL;
getById("inviteLinkURL").innerText = inviteURL;
getById("head9").classList.remove("hidden");
});
} else {
// No password in URL - use default, no param needed
getById("inviteLinkURL").href = inviteURL;
getById("inviteLinkURL").innerText = inviteURL;
getById("head9").classList.remove("hidden");
}
} else {
getById("head2").className = "";
}
} else {
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("legal").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 (session.mobile && session.streamSrc && needsLegacyWakeLock()) {
if (Firefox) {
startLegacyKeepAliveLoop(true);
} else if (!session.avatar) {
try {
if (session.streamSrc.getVideoTracks && !session.streamSrc.getVideoTracks().length) {
startLegacyKeepAliveLoop();
}
} catch (e) {
errorlog(e);
}
}
}
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://") && !electronApi && (location.hostname !== "insecure.vdo.ninja")) {
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;
}
// Add auth parameters if in auth mode
var authParams = "";
if (session.authMode) {
// For view links, we need a universal token that bypasses auth
if (session.universalViewToken) {
authParams = "&universaltoken=" + session.universalViewToken;
} else {
// Fallback: include auth flag so viewer knows auth is required
authParams = "&auth=true";
}
}
if (scene) {
return "https://" + location.host + location.pathname + "?view=" + streamID + "&solo" + codecGroupFlag + "&room=" + session.roomid + passAdd2 + authParams + wss + soloLinkAppended;
} else {
return "https://" + location.host + location.pathname + "?view=" + streamID + codecGroupFlag + passAdd2 + authParams + 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("legal").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 panning (publisher-side): force mono, then pan to stereo
if (session.micPanning !== false) {
anonNode = applyMicPanning(anonNode, webAudio, session.micPanning);
}
// 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 applyMicPanning(inputNode, webAudio, value) {
// Convert 0..180 to -1..1 (90 center)
if (value === true || value === "true") {
value = 90;
}
value = parseFloat(value);
if (isNaN(value)) { value = 90; }
var panNorm = (value / 90.0) - 1.0;
if (panNorm < -1) panNorm = -1;
if (panNorm > 1) panNorm = 1;
// Downmix to mono explicitly
let splitter = webAudio.audioContext.createChannelSplitter(2);
let mono = webAudio.audioContext.createChannelMerger(1);
try {
inputNode.connect(splitter);
splitter.connect(mono, 0, 0);
splitter.connect(mono, 1, 0);
} catch (e) {
// If connection fails (e.g., mono input), fallback to direct
try { inputNode.connect(mono, 0, 0); } catch (ee) { }
}
// Pre-pan gain reduction to avoid clipping when panned
webAudio.micPanGainNode = webAudio.audioContext.createGain();
webAudio.micPanGainNode.gain.value = 1 - Math.abs(panNorm) / 2;
mono.connect(webAudio.micPanGainNode);
// Create panner with Safari fallback
if (webAudio.audioContext.createStereoPanner) {
webAudio.micPanType = "stereo";
webAudio.micPanNode = webAudio.audioContext.createStereoPanner();
webAudio.micPanNode.pan.value = panNorm;
} else {
webAudio.micPanType = "panner";
webAudio.micPanNode = webAudio.audioContext.createPanner();
webAudio.micPanNode.panningModel = "equalpower";
webAudio.micPanNode.distanceModel = "inverse";
let x = panNorm;
let z = 1 - Math.abs(panNorm);
try {
if (typeof webAudio.micPanNode.positionX !== "undefined") {
webAudio.micPanNode.positionX.value = x;
webAudio.micPanNode.positionY.value = 0;
webAudio.micPanNode.positionZ.value = z;
} else {
webAudio.micPanNode.setPosition(x, 0, z);
}
} catch (e) { }
}
webAudio.micPanGainNode.connect(webAudio.micPanNode);
return webAudio.micPanNode;
}
function changeMicPanning(value, deviceid = null) {
// Update all active outbound webAudio chains
let pan = parseFloat(value);
if (isNaN(pan)) { pan = 90; }
if (pan < 0) pan = 0;
if (pan > 180) pan = 180;
let norm = (pan / 90.0) - 1.0;
if (norm < -1) norm = -1;
if (norm > 1) norm = 1;
session.micPanning = pan;
for (var waid in session.webAudios) {
try {
let wa = session.webAudios[waid];
if (!wa) continue;
if (wa.micPanNode) {
if (wa.micPanType === "stereo" && wa.micPanNode.pan) {
wa.micPanNode.pan.setValueAtTime(norm, wa.audioContext.currentTime);
} else {
let x = norm;
let z = 1 - Math.abs(norm);
if (typeof wa.micPanNode.positionX !== "undefined") {
wa.micPanNode.positionX.setValueAtTime(x, wa.audioContext.currentTime);
wa.micPanNode.positionY.setValueAtTime(0, wa.audioContext.currentTime);
wa.micPanNode.positionZ.setValueAtTime(z, wa.audioContext.currentTime);
} else if (wa.micPanNode.setPosition) {
wa.micPanNode.setPosition(x, 0, z);
}
}
}
if (wa.micPanGainNode && wa.micPanGainNode.gain) {
wa.micPanGainNode.gain.setValueAtTime(1 - Math.abs(norm) / 2, wa.audioContext.currentTime);
}
} catch (e) { errorlog(e); }
}
}
// helper to keep approval popup text current with label/streamID
session.updateApprovalPrompt = function (UUID) {
try {
if (!session.director || !session.approval_popup) { return; }
var label = (session.rpcs[UUID] && session.rpcs[UUID].label) || ("Guest " + (UUID || '').substring(0, 8));
var sid = (session.rpcs[UUID] && session.rpcs[UUID].streamID) || UUID;
try { label = ("" + label).replace(/[<>]/g, ""); sid = ("" + sid).replace(/[<>]/g, ""); } catch (e) { }
var line = "A guest is waiting to be admitted.\n\n" +
"Guest: " + label + "\n" +
"ID: " + sid + "\n\n" +
(session.directorState === false ? "Approve?\n(This sends the action to the main director.)" : "Approve?");
updateConfirmAlt('approval-' + UUID, line);
} catch (e) { errorlog(e); }
};
function requestChangeMicPanning(value, UUID, track = 0) {
var msg = {};
msg.requestChangeMicPanning = true;
msg.value = value;
msg.UUID = UUID;
msg.track = track;
session.sendRequest(msg, msg.UUID);
pokeIframeAPI("request-change-micpanning", { value: value, track: track }, UUID);
}
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;
}
async function joinRoom(roomname) {
if (roomname.length) {
roomname = sanitizeRoomName(roomname);
log("Join room: " + roomname);
// In auth mode, use auth-aware room joining
if (session.authMode && window.vdoAuth) {
const hasAccess = await window.vdoAuth.joinRoom(roomname);
if (!hasAccess) {
return; // Access denied or auth required
}
// Room ID might have changed if it was an alias
roomname = session.roomid;
}
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
}
// Create universal token for directors in auth mode
if (session.director && session.authMode && session.authToken && !session.universalViewToken) {
vdoAuth.createUniversalToken().then(() => {
if (session.universalViewToken) {
updateAllSoloLinks();
}
});
}
// Apply any pending room settings selected pre-join (access mode, allowlist)
if (session.director && session.authMode && window.vdoAuth && session.authToken && session.pendingRoomSettings) {
try {
window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, session.pendingRoomSettings);
} catch (e) { console.error(e); }
// Clear once applied
session.pendingRoomSettings = null;
}
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" + invite + "", 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" + invite + "", false, false);
});
}
} else {
var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + token;
warnUser("You can invite others with:\n\n" + invite + "", 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) {
// Only queueType 2 (&screen) sees director immediately.
// queueType 3 (&hold) and 4 (&holdwithvideo) are fully isolated
// from the director until activated.
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) {
// Only queueType 2 (&screen) sees director immediately.
// queueType 3 (&hold) and 4 (&holdwithvideo) are fully isolated
// from the director until activated.
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) : "";
// Pre-join SSO room setup (optional)
try {
var ssoBox = getById('useSSOForRoom');
if (ssoBox && ssoBox.checked) {
// Enable auth mode for this room
session.authMode = true;
// Director should sign in before managing the room
// Note: join gating handled by vdoAuth.joinRoom in joinRoom()
// Capture desired access mode to apply after join
var selected = document.querySelector('input[name="ssoAccessMode"]:checked');
var accessMode = (selected && selected.value) ? selected.value : 'public';
var allowlist = [];
if (accessMode === 'allowlist') {
var csv = (getById('preAllowlistCSV') && getById('preAllowlistCSV').value) ? getById('preAllowlistCSV').value : '';
if (csv) {
allowlist = csv.split(',').map(x => x.trim()).filter(x => x.length > 0);
}
}
// Store to apply after join
session.pendingRoomSettings = { accessMode: accessMode, allowlist: allowlist };
// If guests must sign in (authenticated/allowlist), mark as requireAuth for UX
if (accessMode === 'authenticated' || accessMode === 'allowlist') {
session.requireAuth = true;
}
}
} catch (e) { }
}
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);
}
}
function isLivePeerConnection(pc) {
if (!pc) {
return false;
}
var state = pc.connectionState || pc.iceConnectionState || "";
if (!state) {
return true;
}
state = state.toLowerCase();
return !(state === "failed" || state === "disconnected" || state === "closed");
}
async function checkDirectorStreamID() {
if (session.directorStreamID) {
for (var UUID in session.rpcs) {
if (!isLivePeerConnection(session.rpcs[UUID])) {
continue;
}
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 (!isLivePeerConnection(session.pcs[UUID])) {
continue;
}
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 = ' LINKS (GUEST INVITES & SCENES)';
getById("directorLinks2").style.display = "inline-block";
getById("customizeLinks").classList.remove("hidden");
} else {
ele.innerHTML = ' LINKS (GUEST INVITES & SCENES)';
getById("directorLinks2").style.display = "none";
getById("help_directors_room").style.display = "none";
getById("roomnotes2").style.display = "none";
getById("customizeLinks").classList.add("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;
}
function updateConfirmAlt(context, inputText) {
try {
if (!context) { return; }
var ctx = ("" + context).replace(/["<>]/g, "");
var modal = document.querySelector('.promptModal[data-context="' + ctx + '"]');
if (!modal) { return; }
var text = "" + ("" + inputText).replace("\n", " ") + "";
text = text.replace(/\n/g, " ");
var msg = modal.querySelector('.promptModalMessage');
if (msg) { msg.innerHTML = text; }
} catch (e) { /* noop */ }
}
function toggleCoDirector_approve(ele) {
// UI label: "Allow co-directors to approve held guests"
// Checked means approvals allowed; unchecked means disabled
return;
}
// Route approvals are default; no UI toggle needed anymore.
function toggleApprovalPopup(ele) {
session.approval_popup = ele.checked;
try {
var token = "";
if (session.token) { token += "&token=" + session.token; }
var url = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token;
if (session.approval_popup) { url += "&approvepopup"; }
try { console.log("[flags] toggled approval_popup=" + session.approval_popup + "; co-director invite=" + url); } catch (e) { }
if (session.password !== session.sitePassword) {
if (session.password === false) { url += "&password=false"; }
else { url += "&password=" + session.password; }
}
if (getById("codirectorSettings_invite")) {
getById("codirectorSettings_invite").value = url;
}
} catch (e) { /* noop */ }
}
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.approval_popup) {
getById("codirectorSettings_invite").value += "&approvepopup";
}
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.approval_popup) {
getById("codirectorSettings_invite").value += "&approvepopup";
}
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;
}
// Add auth parameters if in auth mode
var authParams = "";
if (session.authMode) {
authParams = "&auth=true";
// Create universal token for scene links if we're authenticated
if (session.authToken && !session.universalViewToken) {
vdoAuth.createUniversalToken().then(() => {
// Update all links once token is created
if (session.universalViewToken) {
// Update scene link with universal token
var sceneAuthParams = "&universaltoken=" + session.universalViewToken;
getById("director_block_3").dataset.raw = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams;
getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams;
getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams;
// Update all solo links
updateAllSoloLinks();
}
});
}
}
getById("director_block_1").dataset.raw = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams;
getById("director_block_1").href = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams;
getById("director_block_1").innerText = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams;
// For scene links, use universal token if available
var sceneAuthParams = "";
if (session.authMode && session.universalViewToken) {
sceneAuthParams = "&universaltoken=" + session.universalViewToken;
} else if (session.authMode) {
sceneAuthParams = authParams;
}
getById("director_block_3").dataset.raw = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams;
getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams;
getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams;
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("legal").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 = '';
//miniTranslate(getById("miniPerformer"));
// Use soloLinkGenerator to get proper auth parameters
var directorSoloLink = soloLinkGenerator(session.streamID, true);
getById("grabDirectorSoloLink").dataset.raw = directorSoloLink;
getById("grabDirectorSoloLink").href = directorSoloLink;
getById("grabDirectorSoloLink").innerText = directorSoloLink;
getById("grabDirectorSoloLinkParent").classList.remove("hidden");
} else {
getById("miniPerformer").innerHTML = '';
}
miniTranslate(getById("miniPerformer"));
getById("miniPerformer").className = "";
var tabindex = 26;
if (session.rooms && session.rooms.length > 0) {
var container = getById("rooms");
container.innerHTML += 'Arm Transfer: ';
session.rooms.forEach(function (r) {
// if(session.roomid == r) return; //don't include self
container.innerHTML += '";
tabindex++;
});
}
} else {
getById("miniPerformer").style.display = "none";
getById("controlButtons").classList.add("hidden");
// getById("legal").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;
var soloVideoMode = null;
for (var i = 0; i < eles.length; i++) {
if (eles[i].value == 1) {
warnlog(eles[i]);
if (eles[i].dataset.sid) {
if (eles[i].classList && eles[i].classList.contains("altpress")) {
soloVideoMode = "alt";
}
settings.soloVideo = eles[i].dataset.sid; // who is solo, if someone is solo
}
}
}
if (soloVideoMode) {
settings.soloVideoMode = soloVideoMode;
} else {
delete settings.soloVideoMode;
}
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 normalizeLayoutStateValue(state) {
if (typeof state === "undefined") {
return undefined;
}
if (state === null) {
return false;
}
if (state === true) {
return false;
}
if (state === false) {
return false;
}
if (typeof state === "number") {
return state ? state : false;
}
if (typeof state === "string") {
const normalized = state.trim().toLowerCase();
if (!normalized) {
return false;
}
if (normalized === "false" || normalized === "off" || normalized === "auto" || normalized === "0") {
return false;
}
if (normalized === "true") {
return false;
}
}
return state;
}
function isAutoLayoutState(state) {
const normalized = normalizeLayoutStateValue(state);
return normalized === false || typeof normalized === "undefined" || normalized === null;
}
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) {
var layoutState = session.pcs[uuid].layoutState;
if (!session.pcs[uuid].solo && isAutoLayoutState(layoutState)) {
// only issue highlight commands to non-solo links when the scene is auto mixing
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 && session.showDirector) {
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";
var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : "";
// Build HTML with same structure
buttons +=
`
`;
// Sync the initial state
syncSlotState(session.streamID, biggestSlot, false); // false since UI is being created here
}
buttons +=
"
\
";
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 +=
"
\
" +
sanitizeChat(soloLink) +
"\
\
\
";
if (session.directorUUID) {
controls.innerHTML += "
This is you, a co-director. You are also a performer.
";
} else {
controls.innerHTML += "
This is you, the director. You are also a performer.
";
}
}
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 = '";
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";
var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : "";
buttons +=
`
`;
// Sync the initial state
syncSlotState(screenStreamID, biggestSlot, false); // false since UI is being created here
}
buttons +=
"
\
";
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 +=
"
\
" +
sanitizeChat(soloLink) +
"\
\
\
";
if (session.directorUUID) {
controls.innerHTML += "
This is you, a co-director. You are also a performer.
";
}
}
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 = '";
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) {
var tmp2 = target2.querySelector("[data-sid]");
if (tmp2) {
var lock = target2.querySelector("[data-locked]");
if (lock) {
lock = parseInt(lock.dataset.locked);
}
tmp2 = tmp2.dataset.sid;
slots[tmp2] = 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 = "#" + ele.dataset.locked + "";
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 = "";
ele.parentNode.classList.remove("locked");
}
} else {
if (ele.dataset.locked && parseInt(ele.dataset.locked)) {
ele.dataset.locked = 0;
ele.innerHTML = "";
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 = "#" + ele.dataset.locked + "";
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 = `
×${message}
`;
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) {
positionAlertModalNearEvent(document.getElementById("alertModal"), event);
}
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;
applySlotColor(otherSlotBar, 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;
applySlotColor(container, 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;
applySlotColor(container, 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;
applySlotColor(container, 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;
applySlotColor(slotsBar, 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;
applySlotColor(slotBar, 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"));
}
// Remove any existing container with this UUID to prevent stacking
var existingContainer = document.getElementById("container_" + UUID);
if (existingContainer) {
existingContainer.parentNode.removeChild(existingContainer);
}
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 += "";
if (session.rpcs[UUID].pseudoguest) {
controls.querySelectorAll("[data-action-type]").forEach(ele => {
ele.dataset.UUID = UUID;
ele.dataset.sid = streamID;
});
return;
}
//controls.innerHTML += "";
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 += "
Links
"; //Seems to create an empty div.
if (session.hidesololinks == false) {
controls.innerHTML +=
"
\
" +
sanitizeChat(soloLink) +
"\
\
";
}
controls.innerHTML +=
'\
';
controls.innerHTML +=
'\
';
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 && slot_init !== 0) { // slot_init === 0 means guest explicitly opted out of slots
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";
var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : "";
buttons +=
`
`;
// Sync the initial state
syncSlotState(streamID, biggestSlot, false); // false since UI is being created here
}
buttons +=
"
ID: " +
streamID +
"\
\
\
\
";
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('");
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);
// Broadcast updated slots immediately so scenes with &viewslot update
if (session.slotmode && session.director) {
broadcastSlotUpdate();
}
}
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 += "";
//controls.innerHTML += "";
var handsID = "hands_" + UUID;
controls.innerHTML += "
";
if (session.hidesololinks == false) {
controls.innerHTML +=
"
\
" +
sanitizeChat(soloLink) +
"\
\
";
}
controls.innerHTML +=
'\
';
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";
var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : "";
session.pastSlots[streamID] = biggestSlot;
// Build HTML with same structure
buttons +=
`
`;
// Sync the initial state
syncSlotState(streamID, biggestSlot, false); // false since UI is being created here
}
buttons +=
"
ID: " +
streamID +
"\
\
\
\
";
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('");
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");
try {
session.applyQueueStateChange(ele.dataset.UUID, false, "remote-remove-queue");
} catch (e) {
errorlog(e);
}
}
function minimizeMe(button, director = false) {
var container = null;
if (!director) {
container = getById("container_" + button.dataset.UUID);
} else {
container = getById(director);
}
if (!container) {
return;
}
var wasMinimized = container.classList.contains("minimized");
if (!wasMinimized) {
var measuredWidth = container.offsetWidth || container.scrollWidth;
if (measuredWidth) {
container.dataset.minimizedWidth = measuredWidth;
}
}
var isMinimized = container.classList.toggle("minimized");
if (isMinimized) {
var storedWidth = parseFloat(container.dataset.minimizedWidth);
if (!storedWidth) {
storedWidth = container.scrollWidth || container.offsetWidth;
}
if (storedWidth) {
container.style.width = storedWidth + "px";
container.style.minWidth = storedWidth + "px";
}
} else {
container.style.removeProperty("width");
container.style.removeProperty("min-width");
var currentWidth = container.offsetWidth || container.scrollWidth;
if (currentWidth) {
container.dataset.minimizedWidth = currentWidth;
} else {
delete container.dataset.minimizedWidth;
}
}
}
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:
===> " + linkout + "
To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest
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:
===> " + linkout + "
To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest
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:
===> " + linkout + "
To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest
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();
}
// Conservative audio label normalizer to alias Windows "Default -" / "Communications -" prefixed devices
function normalizeAudioAliasLabel(label) {
try {
if (!label) return "";
let s = String(label).trim();
// Normalize case and whitespace
s = s.replace(/^\s+|\s+$/g, "");
// Remove leading Default/Communications prefixes with common separators ("-", ":", em/en dashes)
// Keep the rest intact (do NOT strip digits or other differences)
s = s.replace(/^(?:Default|Communications)\s*[-:\u2013\u2014]?\s*/i, "");
return s.toLowerCase();
} catch (e) {
return String(label || "").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 = ' No Audio';
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") {
var deviceMatched = false;
if (session.audioDevice.includes(deviceInfos[i].deviceId)) {
matched1.push(deviceInfos[i]);
deviceMatched = true;
} else if (session.audioDevice.includes(normalizeDeviceLabel(deviceInfos[i].label))) {
matched1.push(deviceInfos[i]);
deviceMatched = true;
} 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);
deviceMatched = true;
break;
}
}
}
if (!deviceMatched) {
notmatched.push(deviceInfos[i]);
}
} 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;
var addedDeviceIds = new Set(); // Track already added devices
for (let i = 0; i !== deviceInfos.length; ++i) {
var deviceInfo = deviceInfos[i];
if (deviceInfo == null) {
continue;
}
if (deviceInfo.kind === "audioinput") {
// Skip if this device was already added
if (addedDeviceIds.has(deviceInfo.deviceId)) {
log("Skipping duplicate audio device: " + deviceInfo.label);
continue;
}
addedDeviceIds.add(deviceInfo.deviceId);
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];
// Build active audio deviceId and label sets to avoid duplicate selection
const activeAudioIds = new Set();
const activeAudioLabels = new Set();
const activeAudioNormLabels = new Set();
try {
if (session.streamSrc) {
session.streamSrc.getAudioTracks().forEach(function (t) {
try {
if (t.label) {
activeAudioLabels.add(t.label);
activeAudioNormLabels.add(normalizeAudioAliasLabel(t.label));
}
if (t.getSettings) {
const s = t.getSettings();
if (s && s.deviceId) {
activeAudioIds.add(s.deviceId);
}
}
} catch (e) { }
});
}
} catch (e) { }
// Identify normalized labels that have non-default/communications entries
const nonDefaultNormLabelSet = new Set();
try {
for (let i = 0; i !== deviceInfos.length; ++i) {
const d = deviceInfos[i];
if (!d || d.kind !== "audioinput") continue;
const id = (d.deviceId || "").toLowerCase();
if (id !== "default" && id !== "communications" && d.label) {
nonDefaultNormLabelSet.add(normalizeAudioAliasLabel(d.label));
}
}
} catch (e) { }
// Track which normalized labels we've already auto-checked to avoid duplicates
const checkedByNormLabel = new Set();
// Track deviceIds we've already added to avoid duplicate entries from buggy drivers
const addedDeviceIds = new Set();
[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") {
// Deduplicate by deviceId if possible (defensive against buggy drivers)
try {
if (deviceInfo.deviceId && addedDeviceIds.has(deviceInfo.deviceId)) {
log("Skipping duplicate audio device: " + deviceInfo.label);
continue;
}
if (deviceInfo.deviceId) {
addedDeviceIds.add(deviceInfo.deviceId);
}
} catch (e) { }
var option = document.createElement("input");
option.type = "checkbox";
counter++;
var listele = document.createElement("li");
listele.style.display = "none";
// Auto-check selection based on active track deviceId first, fall back to normalized label
try {
let shouldCheck = false;
const devIdLower = (deviceInfo.deviceId || "").toLowerCase();
const normLabel = normalizeAudioAliasLabel(deviceInfo.label || "");
if (activeAudioIds.size && deviceInfo.deviceId && activeAudioIds.has(deviceInfo.deviceId)) {
shouldCheck = true;
} else if (!activeAudioIds.size && deviceInfo.label && activeAudioNormLabels.has(normLabel)) {
// Prefer non-default entries when multiple share a label
const isDefaultish = (devIdLower === "default" || devIdLower === "communications");
if (checkedByNormLabel.has(normLabel)) {
shouldCheck = false;
} else if (isDefaultish && nonDefaultNormLabelSet.has(normLabel)) {
shouldCheck = false;
} else {
shouldCheck = true;
}
}
if (shouldCheck) {
option.checked = true;
listele.style.display = "inherit";
if (normLabel) {
checkedByNormLabel.add(normLabel);
}
}
} catch (e) { }
option.style.display = "none";
option.value = deviceInfo.deviceId || "default";
option.name = "multiselecta" + counter;
option.id = "multiselecta" + counter;
option.dataset.label = deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1);
try { option.dataset.norm = normalizeAudioAliasLabel(option.dataset.label); } catch (e) { }
try { option.dataset.groupId = deviceInfo.groupId || ""; } catch (e) { }
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 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 refreshVideoDevice(UUID = false) {
if (session.screenShareState || session.mediafileShare) {
log("can't refresh video during screenshare or fileshare");
if (UUID) {
var data = {};
data.UUID = UUID;
data.rejected = "can't refresh video during screen or file share";
session.sendMessage(data, data.UUID);
}
return;
}
log("refreshing video device..");
activatedPreview = false;
grabVideo(session.quality, "videosource", "select#videoSource3");
}
function directRefreshVideo(ele) {
var UUID = ele.dataset.UUID;
if (!UUID) { return; }
var data = {};
data.refreshVideo = true;
data.UUID = UUID;
if (session.sendRequest(data, UUID)) {
ele.classList.add("pressed");
setTimeout((ele) => {
if (ele) {
ele.classList.remove("pressed");
}
}, 400, ele);
}
}
function directRefreshConnection(ele) {
var UUID = ele.dataset.UUID;
if (!UUID) { return; }
var data = {};
data.refreshConnection = true;
data.UUID = UUID;
if (session.sendRequest(data, UUID)) {
ele.classList.add("pressed");
setTimeout((ele) => {
if (ele) {
ele.classList.remove("pressed");
}
}, 400, ele);
}
}
function directReconnectPeer(guestUUID, peerUUID) {
// Tell a specific guest to reconnect to a specific peer
var data = {};
data.reconnectPeer = peerUUID;
data.UUID = guestUUID;
session.sendRequest(data, guestUUID);
log("Sent reconnectPeer command to " + guestUUID + " for peer " + peerUUID);
}
// Mesh diagram action wrappers
function meshRefreshVideo(uuid) {
var data = {};
data.refreshVideo = true;
data.UUID = uuid;
session.sendRequest(data, uuid);
log("Sent refreshVideo to " + uuid);
}
function meshRefreshConnection(uuid) {
var data = {};
data.refreshConnection = true;
data.UUID = uuid;
session.sendRequest(data, uuid);
log("Sent refreshConnection (ICE restart) to " + uuid);
}
function meshRefreshAll(uuid) {
var data = {};
data.refreshAll = true;
data.UUID = uuid;
session.sendRequest(data, uuid);
log("Sent refreshAll to " + uuid);
}
function meshRefreshMic(uuid) {
var data = {};
data.refreshMicrophone = true;
data.UUID = uuid;
session.sendRequest(data, uuid);
log("Sent refreshMicrophone to " + uuid);
}
function meshRestartWhip(uuid) {
var data = {};
data.restartWhip = true;
data.UUID = uuid;
session.sendRequest(data, uuid);
log("Sent restartWhip to " + uuid);
}
// ============================================
// MESH NETWORK VISUALIZATION
// ============================================
var meshData = {
nodes: {}, // uuid -> node info
edges: [], // connection info between nodes
pendingResponses: 0,
lastRefresh: 0,
modalOpen: false,
patchedConnections: {}, // "uuidA-uuidB" -> {prevStateA: bool, prevStateB: bool} for connections being relayed via mix-minus
whipStatus: null, // {state, url, connected, reconnectAttempts} - WHIP outbound status
whepConnections: {} // uuid -> {state, connected} - WHEP inbound connections
};
// Patch a failed P2P connection via mix-minus relay
// Director becomes the audio bridge between two guests
function patchConnectionViaMixMinus(uuidA, uuidB) {
if (!session.mixMinusState) {
session.mixMinusState = {};
}
// Initialize mix-minus state for both guests if needed
if (!session.mixMinusState[uuidA]) {
initMixMinusStateForGuest(uuidA);
}
if (!session.mixMinusState[uuidB]) {
initMixMinusStateForGuest(uuidB);
}
// Record previous state before modifying (for proper restore on unpatch)
var wasAExcludedFromB = session.mixMinusState[uuidB].excludeSources.includes(uuidA);
var wasBExcludedFromA = session.mixMinusState[uuidA].excludeSources.includes(uuidB);
var wasAEnabled = session.mixMinusState[uuidA].enabled || false;
var wasBEnabled = session.mixMinusState[uuidB].enabled || false;
// Enable mix-minus for both guests (required for patching to work)
session.mixMinusState[uuidA].enabled = true;
session.mixMinusState[uuidB].enabled = true;
// Remove A from B's excludeSources (so B hears A via director)
var idxAinB = session.mixMinusState[uuidB].excludeSources.indexOf(uuidA);
if (idxAinB > -1) {
session.mixMinusState[uuidB].excludeSources.splice(idxAinB, 1);
}
// Remove B from A's excludeSources (so A hears B via director)
var idxBinA = session.mixMinusState[uuidA].excludeSources.indexOf(uuidB);
if (idxBinA > -1) {
session.mixMinusState[uuidA].excludeSources.splice(idxBinA, 1);
}
// Update the mixes
updateMixMinusForGuest(uuidA);
updateMixMinusForGuest(uuidB);
// Track this patched connection with previous state for proper restore
// Use sorted order so we can correctly restore regardless of call order
var sorted = [uuidA, uuidB].sort();
var patchKey = sorted.join("-");
meshData.patchedConnections[patchKey] = {
// Store as: was sorted[0] excluded from sorted[1]'s mix, and vice versa
wasFirstExcludedFromSecond: sorted[0] === uuidA ? wasAExcludedFromB : wasBExcludedFromA,
wasSecondExcludedFromFirst: sorted[0] === uuidA ? wasBExcludedFromA : wasAExcludedFromB,
// Store enabled state for both guests
wasFirstEnabled: sorted[0] === uuidA ? wasAEnabled : wasBEnabled,
wasSecondEnabled: sorted[0] === uuidA ? wasBEnabled : wasAEnabled
};
log("Patched connection via mix-minus: " + uuidA + " <-> " + uuidB);
}
// Unpatch a connection (when P2P recovers or manually)
function unpatchConnection(uuidA, uuidB) {
if (!session.mixMinusState) return;
var sorted = [uuidA, uuidB].sort();
var patchKey = sorted.join("-");
var savedState = meshData.patchedConnections[patchKey];
// Restore previous exclude state (only add back if they were excluded before patching)
// sorted[0] = first UUID alphabetically, sorted[1] = second
var firstUUID = sorted[0];
var secondUUID = sorted[1];
if (session.mixMinusState[secondUUID] && savedState && savedState.wasFirstExcludedFromSecond) {
// First was excluded from second's mix before - restore that
if (!session.mixMinusState[secondUUID].excludeSources.includes(firstUUID)) {
session.mixMinusState[secondUUID].excludeSources.push(firstUUID);
}
updateMixMinusForGuest(secondUUID);
}
if (session.mixMinusState[firstUUID] && savedState && savedState.wasSecondExcludedFromFirst) {
// Second was excluded from first's mix before - restore that
if (!session.mixMinusState[firstUUID].excludeSources.includes(secondUUID)) {
session.mixMinusState[firstUUID].excludeSources.push(secondUUID);
}
updateMixMinusForGuest(firstUUID);
}
// Restore previous enabled state for both guests
if (savedState) {
if (session.mixMinusState[firstUUID]) {
session.mixMinusState[firstUUID].enabled = savedState.wasFirstEnabled;
updateMixMinusForGuest(firstUUID);
}
if (session.mixMinusState[secondUUID]) {
session.mixMinusState[secondUUID].enabled = savedState.wasSecondEnabled;
updateMixMinusForGuest(secondUUID);
}
}
// Remove from patched tracking
delete meshData.patchedConnections[patchKey];
log("Unpatched connection: " + uuidA + " <-> " + uuidB);
}
// Auto-patch all failed connections in the mesh
function autoPatchAllFailed() {
var patchCount = 0;
meshData.edges.forEach(function(edge) {
if (edge.state === "failed" || edge.state === "disconnected") {
// Skip edges involving director or viewers - patching only makes sense for guest↔guest
var sourceNode = meshData.nodes[edge.source];
var targetNode = meshData.nodes[edge.target];
if (sourceNode && (sourceNode.isDirector || sourceNode.isViewer)) return;
if (targetNode && (targetNode.isDirector || targetNode.isViewer)) return;
var patchKey = [edge.source, edge.target].sort().join("-");
if (!meshData.patchedConnections[patchKey]) {
patchConnectionViaMixMinus(edge.source, edge.target);
patchCount++;
}
}
});
log("Auto-patched " + patchCount + " failed connections via mix-minus");
if (meshData.modalOpen) {
renderMeshVisualization();
}
return patchCount;
}
// Auto-unpatch connections that have recovered
function autoUnpatchRecovered() {
var unpatchCount = 0;
// Collect keys to unpatch first (avoid modifying while iterating)
var toUnpatch = [];
for (var patchKey in meshData.patchedConnections) {
// Find the corresponding edge
var edge = meshData.edges.find(function(e) { return e.id === patchKey; });
if (edge && edge.state === "connected") {
toUnpatch.push(patchKey);
}
}
// Now unpatch collected connections
toUnpatch.forEach(function(patchKey) {
var uuids = patchKey.split("-");
unpatchConnection(uuids[0], uuids[1]);
unpatchCount++;
});
if (unpatchCount > 0) {
log("Auto-unpatched " + unpatchCount + " recovered connections");
if (meshData.modalOpen) {
renderMeshVisualization();
}
}
return unpatchCount;
}
// Check if a connection is currently patched
function isConnectionPatched(uuidA, uuidB) {
var patchKey = [uuidA, uuidB].sort().join("-");
return !!meshData.patchedConnections[patchKey];
}
// UI wrapper for patching from mesh diagram
function meshPatchConnection(uuidA, uuidB) {
patchConnectionViaMixMinus(uuidA, uuidB);
// Refresh the edge details panel
var edgeId = [uuidA, uuidB].sort().join("-");
var edge = meshData.edges.find(function(e) { return e.id === edgeId; });
if (edge) {
showEdgeDetails(edge);
}
renderMeshVisualization();
}
// UI wrapper for unpatching from mesh diagram
function meshUnpatchConnection(uuidA, uuidB) {
unpatchConnection(uuidA, uuidB);
// Refresh the edge details panel
var edgeId = [uuidA, uuidB].sort().join("-");
var edge = meshData.edges.find(function(e) { return e.id === edgeId; });
if (edge) {
showEdgeDetails(edge);
}
renderMeshVisualization();
}
function requestMeshData() {
// Request connection maps from all connected guests
meshData.nodes = {};
meshData.edges = [];
meshData.pendingResponses = 0;
// Collect WHIP outbound status
meshData.whipStatus = null;
if (session.whipOut) {
meshData.whipStatus = {
state: session.whipOut.connectionState || session.whipOut.iceConnectionState || "unknown",
url: session.whipOutput || "",
connected: session.whipOut.connectionState === 'connected' ||
session.whipOut.iceConnectionState === 'connected' ||
session.whipOut.iceConnectionState === 'completed',
reconnectAttempts: session.getWhipReconnectAttempts ? session.getWhipReconnectAttempts() : 0
};
}
// Collect WHEP inbound connection statuses
meshData.whepConnections = {};
for (var uuid in session.rpcs) {
if (session.rpcs[uuid] && session.rpcs[uuid].whep) {
meshData.whepConnections[uuid] = {
state: session.rpcs[uuid].whep.connectionState || session.rpcs[uuid].whep.iceConnectionState || "unknown",
connected: session.rpcs[uuid].whep.connectionState === 'connected' ||
session.rpcs[uuid].whep.iceConnectionState === 'connected' ||
session.rpcs[uuid].whep.iceConnectionState === 'completed'
};
}
}
// Add director as a node - include its connections from rpcs and pcs
var directorConnections = [];
// Director's rpcs = guests publishing TO director = director receives from them
for (var uuid in session.rpcs) {
if (session.rpcs[uuid]) {
var rpc = session.rpcs[uuid];
directorConnections.push({
peerUUID: uuid,
peerStreamID: rpc.streamID || uuid,
direction: "incoming", // Director receives from guest
state: rpc.connectionState || "connected",
bandwidth: rpc.bandwidth || -1,
audioEnabled: true,
videoEnabled: true
});
}
}
// Director's pcs = director publishing TO peers (data channels, etc.)
for (var uuid in session.pcs) {
if (session.pcs[uuid]) {
var pc = session.pcs[uuid];
directorConnections.push({
peerUUID: uuid,
peerStreamID: pc.streamID || uuid,
direction: "outgoing", // Director sends to guest/scene
state: pc.connectionState || "connected",
bandwidth: -1,
audioEnabled: true,
videoEnabled: true
});
}
}
meshData.nodes[session.UUID] = {
uuid: session.UUID,
streamID: session.streamID,
label: "Director",
isDirector: true,
connections: directorConnections,
health: "healthy"
};
// Request from all rpcs (guests we're receiving from)
for (var uuid in session.rpcs) {
if (session.rpcs[uuid]) {
var data = { getConnectionMap: true, UUID: uuid };
session.sendRequest(data, uuid);
meshData.pendingResponses++;
// Add node placeholder
meshData.nodes[uuid] = {
uuid: uuid,
streamID: session.rpcs[uuid].streamID || uuid,
label: session.rpcs[uuid].label || session.rpcs[uuid].streamID || "Guest",
isDirector: false,
connections: [],
health: "pending"
};
}
}
log("Requested mesh data from " + meshData.pendingResponses + " guests");
// Set timeout to process after responses come in
setTimeout(function() {
aggregateMeshData();
renderMeshVisualization();
}, 2000);
}
function handleConnectionMapResponse(msg, UUID) {
// Called when a guest responds with their connection map
// UUID = the key from director's rpcs (how director identifies this guest)
// msg.connectionMap.uuid = guest's session.UUID (might differ!)
if (msg.connectionMap) {
var map = msg.connectionMap;
// Use the UUID parameter (director's key) for node matching, not map.uuid
// This ensures we update the correct placeholder node
var nodeKey = UUID;
// Store the director's external UUID (as known by this guest)
// This lets us map guest connections to director correctly
if (map.requesterUUID) {
meshData.directorExternalUUID = map.requesterUUID;
}
// Check if any connections use TURN (relay)
var usingTurn = false;
if (map.connections) {
for (var i = 0; i < map.connections.length; i++) {
if (map.connections[i].candidateType === "relay") {
usingTurn = true;
break;
}
}
}
// Update node info using director's UUID key
if (meshData.nodes[nodeKey]) {
meshData.nodes[nodeKey].streamID = map.streamID;
meshData.nodes[nodeKey].label = map.label;
meshData.nodes[nodeKey].guestUUID = map.uuid; // Store guest's self-reported UUID
meshData.nodes[nodeKey].connections = map.connections;
meshData.nodes[nodeKey].browser = map.browser || "Unknown";
meshData.nodes[nodeKey].usingTurn = usingTurn;
} else {
meshData.nodes[nodeKey] = {
uuid: nodeKey,
guestUUID: map.uuid,
streamID: map.streamID,
label: map.label,
isDirector: false,
connections: map.connections,
browser: map.browser || "Unknown",
usingTurn: usingTurn,
health: "healthy"
};
}
meshData.pendingResponses--;
log("Received connection map from " + map.label + " (UUID: " + nodeKey + ", " + map.connections.length + " connections)");
// Re-render whenever a response arrives and modal is open
// This handles late responses even if some peers never respond
if (meshData.modalOpen) {
aggregateMeshData();
renderMeshVisualization();
}
}
}
function aggregateMeshData() {
// Build edges from all node connections
meshData.edges = [];
var edgeMap = {}; // edgeId -> edge object (to track bidirectionality)
// Build a lookup from streamID to node UUID for edge matching
var streamIdToUuid = {};
var directorNodeKey = null;
for (var uuid in meshData.nodes) {
var node = meshData.nodes[uuid];
if (node.streamID) {
streamIdToUuid[node.streamID] = uuid;
}
if (node.isDirector) {
directorNodeKey = uuid;
}
}
for (var uuid in meshData.nodes) {
var node = meshData.nodes[uuid];
var failedCount = 0;
var degradedCount = 0;
if (node.connections) {
for (var i = 0; i < node.connections.length; i++) {
var conn = node.connections[i];
// Try to resolve peer by streamID first (more reliable), then UUID
var peerNodeUuid = conn.peerUUID;
if (conn.peerStreamID && streamIdToUuid[conn.peerStreamID]) {
peerNodeUuid = streamIdToUuid[conn.peerStreamID];
}
// If peer doesn't exist as a node and this is an outgoing connection,
// check if it's actually the director (using directorExternalUUID)
if (!meshData.nodes[peerNodeUuid] && conn.direction === "outgoing") {
// Check if this is a connection to the director
if (meshData.directorExternalUUID && conn.peerUUID === meshData.directorExternalUUID) {
// Map to director node instead of creating phantom
peerNodeUuid = directorNodeKey;
} else {
// Create viewer/scene node for other unknown peers
meshData.nodes[peerNodeUuid] = {
uuid: peerNodeUuid,
streamID: conn.peerStreamID || peerNodeUuid,
label: conn.peerStreamID || "Viewer",
isDirector: false,
isViewer: true,
connections: [],
health: "healthy"
};
if (conn.peerStreamID) {
streamIdToUuid[conn.peerStreamID] = peerNodeUuid;
}
}
}
// Create unique edge ID (sorted UUIDs for deduplication)
var edgeId = [uuid, peerNodeUuid].sort().join("-");
// Track direction: outgoing = publishing TO peer, incoming = receiving FROM peer
var directionKey = conn.direction === "outgoing" ? "hasOutgoing" : "hasIncoming";
if (!edgeMap[edgeId]) {
edgeMap[edgeId] = {
id: edgeId,
source: uuid,
target: peerNodeUuid,
sourceStreamID: node.streamID,
targetStreamID: conn.peerStreamID,
state: conn.state,
bandwidth: conn.bandwidth,
candidateType: conn.candidateType,
nackCount: conn.nackCount,
pliCount: conn.pliCount,
hasOutgoing: false,
hasIncoming: false,
bidirectional: false
};
}
// Mark this direction as present
edgeMap[edgeId][directionKey] = true;
// Update bidirectional flag
if (edgeMap[edgeId].hasOutgoing && edgeMap[edgeId].hasIncoming) {
edgeMap[edgeId].bidirectional = true;
}
// Merge states - keep the worse one
var stateRank = { "failed": 0, "disconnected": 1, "new": 1, "connecting": 1, "closed": 1, "connected": 2 };
var existingRank = stateRank[edgeMap[edgeId].state] !== undefined ? stateRank[edgeMap[edgeId].state] : 1;
var newRank = stateRank[conn.state] !== undefined ? stateRank[conn.state] : 1;
if (newRank < existingRank) {
edgeMap[edgeId].state = conn.state;
}
// Track health based on RTCPeerConnection.connectionState
if (conn.state === "failed") {
failedCount++;
} else if (conn.state === "disconnected" || conn.state === "new" || conn.state === "connecting" || conn.state === "closed") {
degradedCount++;
}
}
}
// Update node health
if (failedCount > 0) {
node.health = "failed";
} else if (degradedCount > 0) {
node.health = "degraded";
} else if (node.connections && node.connections.length > 0) {
node.health = "healthy";
} else if (node.isViewer || node.isDirector) {
// Viewers/scenes and director don't report connections, so no connections is expected
node.health = "healthy";
} else {
node.health = "isolated";
}
}
// Convert edgeMap to array
meshData.edges = Object.values(edgeMap);
// Calculate summary stats
var totalConnections = meshData.edges.length;
var failedConnections = meshData.edges.filter(e => e.state === "failed").length;
var healthyConnections = meshData.edges.filter(e => e.state === "connected").length;
var bidirectionalCount = meshData.edges.filter(e => e.bidirectional).length;
var onewayCount = totalConnections - bidirectionalCount;
var viewerCount = Object.values(meshData.nodes).filter(n => n.isViewer).length;
meshData.summary = {
totalNodes: Object.keys(meshData.nodes).length,
viewerNodes: viewerCount,
totalConnections: totalConnections,
bidirectionalConnections: bidirectionalCount,
onewayConnections: onewayCount,
healthyConnections: healthyConnections,
failedConnections: failedConnections,
degradedConnections: totalConnections - healthyConnections - failedConnections
};
meshData.lastRefresh = Date.now();
log("Aggregated mesh data: " + meshData.summary.totalNodes + " nodes, " + meshData.summary.totalConnections + " connections");
}
function getMeshHealthBadge() {
// Returns HTML for the health badge to show in director controls
var s = meshData.summary;
if (!s) return "";
var color = "#4CAF50"; // green
var text = s.healthyConnections + "/" + s.totalConnections;
if (s.failedConnections > 0) {
color = "#F44336"; // red
text = s.failedConnections + " failed";
} else if (s.degradedConnections > 0) {
color = "#FF9800"; // orange
}
return '' + text + '';
}
function openMeshVisualization() {
if (meshData.modalOpen) return;
meshData.modalOpen = true;
// Create modal overlay
var modal = document.createElement("div");
modal.id = "meshModal";
modal.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:10000;display:flex;flex-direction:column;";
// Toolbar
var toolbar = document.createElement("div");
toolbar.style.cssText = "padding:10px 20px;background:#222;display:flex;align-items:center;gap:10px;border-bottom:1px solid #444;flex-wrap:wrap;";
toolbar.innerHTML = `
Mesh Network Debug
`;
// SVG container
var svgContainer = document.createElement("div");
svgContainer.id = "meshSvgContainer";
svgContainer.style.cssText = "flex:1;overflow:hidden;position:relative;";
// Detail panel (hidden by default)
var detailPanel = document.createElement("div");
detailPanel.id = "meshDetailPanel";
detailPanel.style.cssText = "position:absolute;right:0;top:0;width:300px;height:100%;background:#1a1a1a;border-left:1px solid #444;padding:20px;display:none;overflow-y:auto;color:#fff;";
svgContainer.appendChild(detailPanel);
modal.appendChild(toolbar);
modal.appendChild(svgContainer);
document.body.appendChild(modal);
// Initialize layout button text to match current mode
var layoutBtn = document.getElementById("meshLayoutBtn");
if (layoutBtn) {
layoutBtn.textContent = "Layout: " + meshLayoutMode.charAt(0).toUpperCase() + meshLayoutMode.slice(1);
}
// Add keyboard shortcuts
document.addEventListener("keydown", meshKeyHandler);
// Request fresh data and render
requestMeshData();
}
function closeMeshVisualization() {
var modal = document.getElementById("meshModal");
if (modal) {
modal.remove();
}
meshData.modalOpen = false;
document.removeEventListener("keydown", meshKeyHandler);
}
function meshKeyHandler(e) {
if (!meshData.modalOpen) return;
switch(e.key) {
case "Escape":
closeMeshVisualization();
break;
case "r":
case "R":
requestMeshData();
break;
case "f":
case "F":
var cb = document.getElementById("meshFilterProblems");
if (cb) cb.checked = !cb.checked;
renderMeshVisualization();
break;
}
}
var meshLayoutMode = "circular"; // circular, grid, force
function cycleMeshLayout() {
if (meshLayoutMode === "force") {
meshLayoutMode = "circular";
} else if (meshLayoutMode === "circular") {
meshLayoutMode = "grid";
} else {
meshLayoutMode = "force";
}
var btn = document.getElementById("meshLayoutBtn");
if (btn) {
btn.textContent = "Layout: " + meshLayoutMode.charAt(0).toUpperCase() + meshLayoutMode.slice(1);
}
renderMeshVisualization();
}
function renderMeshVisualization() {
var container = document.getElementById("meshSvgContainer");
if (!container) return;
var existingSvg = container.querySelector("svg");
if (existingSvg) existingSvg.remove();
var width = container.clientWidth;
var height = container.clientHeight - 50; // Leave room for status bar
// Create SVG
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", width);
svg.setAttribute("height", height);
svg.style.display = "block";
// Filter problems only?
var filterProblems = document.getElementById("meshFilterProblems")?.checked || false;
// Calculate node positions based on layout mode
var nodePositions = {};
var nodeArray = Object.values(meshData.nodes);
var filteredNodes = filterProblems ? nodeArray.filter(n => n.health !== "healthy") : nodeArray;
if (filteredNodes.length === 0 && filterProblems) {
// Show message
var text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", width / 2);
text.setAttribute("y", height / 2);
text.setAttribute("fill", "#4CAF50");
text.setAttribute("text-anchor", "middle");
text.setAttribute("font-size", "24");
text.textContent = "All connections healthy!";
svg.appendChild(text);
container.insertBefore(svg, container.firstChild);
return;
}
// Calculate positions
var centerX = width / 2;
var centerY = height / 2;
var radius = Math.min(width, height) / 2 - 100;
if (meshLayoutMode === "circular") {
// Director in center, guests in a ring
var guestNodes = filteredNodes.filter(n => !n.isDirector);
var directorNode = filteredNodes.find(n => n.isDirector);
// Place director in center
if (directorNode) {
nodePositions[directorNode.uuid] = { x: centerX, y: centerY };
}
// Place guests in a ring around director
guestNodes.forEach(function(node, i) {
var angle = (2 * Math.PI * i) / guestNodes.length - Math.PI / 2;
nodePositions[node.uuid] = {
x: centerX + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle)
};
});
} else if (meshLayoutMode === "grid") {
var cols = Math.ceil(Math.sqrt(filteredNodes.length));
var spacing = Math.min(width, height) / (cols + 1);
filteredNodes.forEach(function(node, i) {
var col = i % cols;
var row = Math.floor(i / cols);
nodePositions[node.uuid] = {
x: spacing + col * spacing,
y: spacing + row * spacing
};
});
} else {
// Force-directed: simple random for now
filteredNodes.forEach(function(node) {
nodePositions[node.uuid] = {
x: 100 + Math.random() * (width - 200),
y: 100 + Math.random() * (height - 200)
};
});
}
// Add arrow marker definitions for one-way connections
var defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
// Arrow markers for different colors
["#4CAF50", "#FF9800", "#F44336"].forEach(function(color, idx) {
var marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
marker.setAttribute("id", "arrow-" + idx);
marker.setAttribute("markerWidth", "10");
marker.setAttribute("markerHeight", "10");
marker.setAttribute("refX", "35");
marker.setAttribute("refY", "3");
marker.setAttribute("orient", "auto");
marker.setAttribute("markerUnits", "strokeWidth");
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M0,0 L0,6 L9,3 z");
path.setAttribute("fill", color);
marker.appendChild(path);
defs.appendChild(marker);
});
svg.appendChild(defs);
// Draw edges first (so they appear behind nodes)
meshData.edges.forEach(function(edge) {
var sourcePos = nodePositions[edge.source];
var targetPos = nodePositions[edge.target];
if (!sourcePos || !targetPos) return;
// Filter if needed
if (filterProblems && edge.state === "connected") return;
// For one-way connections, determine correct arrow direction
// hasOutgoing means source publishes TO target (arrow: source→target)
// hasIncoming only means target publishes TO source (arrow: target→source)
var drawFromPos = sourcePos;
var drawToPos = targetPos;
if (!edge.bidirectional && edge.hasIncoming && !edge.hasOutgoing) {
// Swap direction - target is actually the publisher
drawFromPos = targetPos;
drawToPos = sourcePos;
}
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", drawFromPos.x);
line.setAttribute("y1", drawFromPos.y);
line.setAttribute("x2", drawToPos.x);
line.setAttribute("y2", drawToPos.y);
// Check if this edge is patched via mix-minus
var isPatched = isConnectionPatched(edge.source, edge.target);
// Style based on state
var strokeColor = "#4CAF50"; // green
var strokeWidth = 2;
var dashArray = "";
var arrowIdx = 0;
if (edge.state === "failed") {
strokeColor = "#F44336";
strokeWidth = 3;
dashArray = "5,5";
arrowIdx = 2;
} else if (edge.state === "disconnected" || edge.state === "new" || edge.state === "connecting" || edge.state === "closed") {
strokeColor = "#FF9800";
dashArray = "10,5";
arrowIdx = 1;
}
// Override style for patched connections - show as cyan with double-dash
if (isPatched) {
strokeColor = "#00BCD4"; // cyan - matches patch button
strokeWidth = 3;
dashArray = "8,3,2,3"; // distinctive double-dash pattern
}
line.setAttribute("stroke", strokeColor);
line.setAttribute("stroke-width", strokeWidth);
if (dashArray) line.setAttribute("stroke-dasharray", dashArray);
line.setAttribute("data-edge-id", edge.id);
line.style.cursor = "pointer";
// Add arrow for one-way connections (not bidirectional)
if (!edge.bidirectional) {
line.setAttribute("marker-end", "url(#arrow-" + arrowIdx + ")");
}
// Click handler for edge
line.onclick = function() {
showEdgeDetails(edge);
};
// Hover effect
line.onmouseenter = function() {
this.setAttribute("stroke-width", parseInt(strokeWidth) + 2);
};
line.onmouseleave = function() {
this.setAttribute("stroke-width", strokeWidth);
};
svg.appendChild(line);
});
// Draw nodes
filteredNodes.forEach(function(node) {
var pos = nodePositions[node.uuid];
if (!pos) return;
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttribute("transform", "translate(" + pos.x + "," + pos.y + ")");
g.style.cursor = "pointer";
// Node shape - circle for publishers, square for viewers/scenes
var shape;
if (node.isViewer) {
// Square for viewers/scenes
shape = document.createElementNS("http://www.w3.org/2000/svg", "rect");
shape.setAttribute("x", -25);
shape.setAttribute("y", -25);
shape.setAttribute("width", 50);
shape.setAttribute("height", 50);
shape.setAttribute("rx", 5);
shape.setAttribute("fill", "#222");
} else {
// Circle for publishers
shape = document.createElementNS("http://www.w3.org/2000/svg", "circle");
shape.setAttribute("r", 30);
shape.setAttribute("fill", "#333");
}
// Border color based on health/type
var borderColor = "#4CAF50";
if (node.health === "failed") borderColor = "#F44336";
else if (node.health === "degraded") borderColor = "#FF9800";
else if (node.health === "isolated") borderColor = "#9E9E9E";
else if (node.isDirector) borderColor = "#2196F3";
else if (node.isViewer) borderColor = "#9C27B0"; // Purple for viewers/scenes
shape.setAttribute("stroke", borderColor);
shape.setAttribute("stroke-width", 3);
// Label
var text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("text-anchor", "middle");
text.setAttribute("dy", 5);
text.setAttribute("fill", "#fff");
text.setAttribute("font-size", "12");
text.textContent = (node.label || "?").substring(0, 8);
// Badge for special node types
if (node.isDirector) {
var badge = document.createElementNS("http://www.w3.org/2000/svg", "text");
badge.setAttribute("text-anchor", "middle");
badge.setAttribute("y", -35);
badge.setAttribute("fill", "#2196F3");
badge.setAttribute("font-size", "10");
badge.textContent = "DIRECTOR";
g.appendChild(badge);
} else if (node.isViewer) {
var badge = document.createElementNS("http://www.w3.org/2000/svg", "text");
badge.setAttribute("text-anchor", "middle");
badge.setAttribute("y", -30);
badge.setAttribute("fill", "#9C27B0");
badge.setAttribute("font-size", "10");
badge.textContent = "SCENE/VIEW";
g.appendChild(badge);
}
g.appendChild(shape);
g.appendChild(text);
// Click handler
g.onclick = function() {
showNodeDetails(node);
};
svg.appendChild(g);
});
container.insertBefore(svg, container.firstChild);
// Add status bar
var statusBar = container.querySelector(".meshStatusBar");
if (!statusBar) {
statusBar = document.createElement("div");
statusBar.className = "meshStatusBar";
statusBar.style.cssText = "position:absolute;bottom:0;left:0;right:0;padding:10px 20px;background:#222;color:#fff;font-size:14px;";
container.appendChild(statusBar);
}
var s = meshData.summary || {};
var nodeInfo = s.totalNodes || 0;
if (s.viewerNodes > 0) {
nodeInfo += " (" + s.viewerNodes + " scenes/viewers)";
}
var connInfo = "";
if (s.bidirectionalConnections > 0) {
connInfo += s.bidirectionalConnections + " bidirectional";
}
if (s.onewayConnections > 0) {
if (connInfo) connInfo += ", ";
connInfo += s.onewayConnections + " one-way→";
}
// Build WHIP/WHEP status display
var whipWhepStatus = "";
if (meshData.whipStatus) {
var whipColor = meshData.whipStatus.connected ? "#4CAF50" : "#F44336";
var whipIcon = meshData.whipStatus.connected ? "la-broadcast-tower" : "la-exclamation-triangle";
whipWhepStatus += '';
whipWhepStatus += ' WHIP: ' + meshData.whipStatus.state;
if (!meshData.whipStatus.connected) {
whipWhepStatus += ' ';
}
if (meshData.whipStatus.reconnectAttempts > 0) {
whipWhepStatus += ' (' + meshData.whipStatus.reconnectAttempts + ' retries)';
}
whipWhepStatus += '';
}
// Count WHEP connection issues
var whepTotal = Object.keys(meshData.whepConnections).length;
var whepFailed = Object.values(meshData.whepConnections).filter(function(w) { return !w.connected; }).length;
if (whepTotal > 0) {
var whepColor = whepFailed === 0 ? "#4CAF50" : "#F44336";
whipWhepStatus += '';
whipWhepStatus += ' WHEP: ' + (whepTotal - whepFailed) + '/' + whepTotal + ' connected';
whipWhepStatus += '';
}
statusBar.innerHTML = `
${s.healthyConnections || 0} healthy |
${s.degradedConnections || 0} degraded |
${s.failedConnections || 0} failed |
${nodeInfo} nodes | ${connInfo || "0 connections"}
${whipWhepStatus}
Last refresh: ${meshData.lastRefresh ? new Date(meshData.lastRefresh).toLocaleTimeString() : "Never"}
`;
}
function showNodeDetails(node) {
var panel = document.getElementById("meshDetailPanel");
if (!panel) return;
panel.style.display = "block";
var connections = node.connections || [];
var connHtml = connections.map(function(c) {
var stateColor = c.state === "connected" ? "#4CAF50" : c.state === "failed" ? "#F44336" : "#FF9800";
var turnBadge = c.candidateType === "relay" ? ' TURN' : '';
return `
`;
}
document.body.appendChild(selectionElem);
const systemAudioCheckbox = getById("alsoCaptureAudio");
const appAudioOption = getById("captureAppAudioParent");
const appAudioCheckbox = getById("captureAppAudio");
if (appAudioOption && appAudioCheckbox) {
const supportsAppAudio = !macOS && electronSupportsApplicationAudio();
if (supportsAppAudio) {
appAudioOption.style.display = "inline-block";
appAudioCheckbox.addEventListener("change", () => {
if (!systemAudioCheckbox) {
return;
}
if (appAudioCheckbox.checked) {
systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio = systemAudioCheckbox.checked ? "true" : "false";
if (systemAudioCheckbox.checked) {
systemAudioCheckbox.checked = false;
}
} else {
if (systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio === "true") {
systemAudioCheckbox.checked = true;
}
delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio;
}
});
} else {
appAudioOption.style.display = "none";
appAudioCheckbox.checked = false;
}
}
if (systemAudioCheckbox) {
systemAudioCheckbox.addEventListener("change", () => {
if (systemAudioCheckbox.checked && appAudioCheckbox) {
appAudioCheckbox.checked = false;
}
delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio;
});
}
if (macOS) {
const captureDesktopButton = getById("captureDesktopAudio");
if (captureDesktopButton) {
captureDesktopButton.style.display = "none";
}
const alsoCaptureAudioCheckbox = getById("alsoCaptureAudio");
if (alsoCaptureAudioCheckbox) {
alsoCaptureAudioCheckbox.checked = false;
}
const alsoCaptureAudioParent1 = getById("alsoCaptureAudioParent1");
if (alsoCaptureAudioParent1) {
alsoCaptureAudioParent1.style.display = "none";
}
const alsoCaptureAudioParent2 = getById("alsoCaptureAudioParent2");
if (alsoCaptureAudioParent2) {
alsoCaptureAudioParent2.style.display = "inline-block";
}
if (appAudioOption) {
appAudioOption.style.display = "none";
}
if (appAudioCheckbox) {
appAudioCheckbox.checked = false;
}
}
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") {
const stream = await createElectronDesktopAudioStream();
resolve(stream);
selectionElem.remove();
return;
}
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`);
}
const systemAudioCheckbox = getById("alsoCaptureAudio");
const appAudioCheckbox = getById("captureAppAudio");
const hadSystemAudioPreference = !!(systemAudioCheckbox && (systemAudioCheckbox.checked || systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio === "true"));
const wantsAppAudio = !!(appAudioCheckbox && appAudioCheckbox.checked && electronSupportsApplicationAudio());
const wantsSystemAudio = !wantsAppAudio && !!(systemAudioCheckbox && systemAudioCheckbox.checked);
var audioStream = null;
if (wantsSystemAudio) {
audioStream = await createElectronDesktopAudioStream();
}
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) { }
if (typeof warnlog === "function") {
warnlog(new_constraints);
warnlog("navigator.mediaDevices.getUserMedia starting...");
}
const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
let attachedAppAudio = false;
if (wantsAppAudio) {
attachedAppAudio = await attachElectronApplicationAudio(stream, source);
if (!attachedAppAudio && hadSystemAudioPreference) {
if (!audioStream) {
try {
audioStream = await createElectronDesktopAudioStream();
} catch (audioErr) {
console.warn("Failed to fallback to desktop audio:", audioErr);
}
}
if (audioStream) {
console.warn("Falling back to desktop audio; application audio capture unavailable for selected source.");
if (systemAudioCheckbox) {
systemAudioCheckbox.checked = true;
delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio;
}
if (appAudioCheckbox) {
appAudioCheckbox.checked = false;
}
}
}
}
if (!attachedAppAudio && audioStream && typeof audioStream.getAudioTracks === "function") {
const audioTracks = audioStream.getAudioTracks();
if (audioTracks.length) {
stream.addTrack(audioTracks[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?)");
}
}
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.screensharefps !== false) {
constraints.video.frameRate = {
ideal: session.screensharefps,
max: session.screensharefps
};
} else 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) {
log("remove ss track clone 11");
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);
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);
log("remove ss track clone 14");
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);
log("remove ss track 21");
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 = ``;
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 togglePTZControls(UUID) {
var modal = getById("ptzControlsModal");
if (UUID) {
modal.dataset.UUID = UUID;
}
toggle(modal);
if (modal.style.display == "none") {
if (getById("modalBackdrop")) {
getById("modalBackdrop").innerHTML = "";
getById("modalBackdrop").remove();
}
} else {
if (getById("modalBackdrop")) {
getById("modalBackdrop").innerHTML = "";
getById("modalBackdrop").remove();
}
zindex = 25;
modal.style.zIndex = 25;
var modalTemplate = ``;
document.body.insertAdjacentHTML("beforeend", modalTemplate);
document.getElementById("modalBackdrop").addEventListener("click", function() {
togglePTZControls();
});
var targetUUID = modal.dataset.UUID;
// Reset sliders to neutral positions
getById("ptzPanSlider").value = 0;
getById("ptzPanValue").innerText = "0";
getById("ptzTiltSlider").value = 0;
getById("ptzTiltValue").innerText = "0";
getById("ptzZoomSlider").value = 50;
getById("ptzZoomValue").innerText = "50";
getById("ptzFocusSlider").value = 0;
getById("ptzFocusValue").innerText = "0";
// Pan slider handlers
getById("ptzPanSlider").oninput = function(e) {
getById("ptzPanValue").innerText = e.target.value;
};
getById("ptzPanSlider").onchange = function(e) {
var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1
session.requestPanChange(normalizedValue, targetUUID, session.remote, true);
};
// Tilt slider handlers
getById("ptzTiltSlider").oninput = function(e) {
getById("ptzTiltValue").innerText = e.target.value;
};
getById("ptzTiltSlider").onchange = function(e) {
var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1
session.requestTiltChange(normalizedValue, targetUUID, session.remote, true);
};
// Zoom slider handlers
getById("ptzZoomSlider").oninput = function(e) {
getById("ptzZoomValue").innerText = e.target.value;
};
getById("ptzZoomSlider").onchange = function(e) {
var normalizedValue = parseInt(e.target.value) / 100; // Convert 0..100 to 0..1
session.requestZoomChange(normalizedValue, targetUUID, session.remote, true);
};
// Focus slider handlers
getById("ptzFocusSlider").oninput = function(e) {
getById("ptzFocusValue").innerText = e.target.value;
};
getById("ptzFocusSlider").onchange = function(e) {
var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1
session.requestFocusChange(normalizedValue, targetUUID, session.remote, true);
};
// Reset Autofocus button handler
getById("ptzResetAutofocusBtn").onclick = function() {
session.requestAutofocusChange(true, targetUUID, session.remote);
};
}
}
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 = ``;
// 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";
}
// Show auth access control if in auth mode and user is director
if (session.authMode && session.director && window.vdoAuth) {
getById("authAccessControl").style.display = "block";
loadRoomAccessSettings();
}
}
}
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 getAudioUserMediaRequestID = 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);
log("remove ss track 9");
track.stop();
wasDisabled = false;
});
}
if (session.streamSrcClone) {
session.streamSrcClone.getVideoTracks().forEach(function (track) {
session.streamSrcClone.removeTrack(track);
log("remove ss track s9");
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);
log("remove ss track 98");
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 && needsLegacyWakeLock()) { // OBSOLETE since we now have "WAKE LOCK" API used.
startLegacyKeepAliveLoop();
}
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(" 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(" 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);
log("remove ss track 84");
//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 || session.screenShareState);
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;
getAudioUserMediaRequestID += 1;
var gumAudioID = getAudioUserMediaRequestID;
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);
log("remove ss track67");
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;
}
log("remove ss track 55");
session.streamSrcClone.removeTrack(track);
track.stop();
});
}
} catch (e) {
errorlog(e);
}
var streams = await getAudioOnly(selector, trackid, override, gumAudioID); // Get audio streams
if (gumAudioID !== getAudioUserMediaRequestID) {
try {
streams.forEach(stream => {
if (!stream) {
return;
}
stream.getTracks().forEach(track => track.stop());
});
} catch (e) { }
activatedPreview = false;
return;
}
try {
log("STREAMS: " + streams.length);
for (var i = 0; i < streams.length; i++) {
streams[i].getAudioTracks().forEach(function (track) {
try {
if (gumAudioID !== getAudioUserMediaRequestID) {
track.stop();
return;
}
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);
try {
if (session.streamSrc && session.streamSrc.getVideoTracks && session.streamSrc.getVideoTracks().length) {
var previewVideoCount = 0;
if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks) {
previewVideoCount = session.videoElement.srcObject.getVideoTracks().length;
}
if (!previewVideoCount && typeof updateRenderOutpipe === "function") {
updateRenderOutpipe();
}
}
} catch (e) {
errorlog(e);
}
}
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 = true; // Chrome 133+ fix: must enable before disabling
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;
if (session.transcript) {
setTimeout(function () {
setupClosedCaptions();
}, 1000);
}
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();
if (session.autorecord || session.autorecordlocal) {
log("AUTO RECORD START");
setTimeout(
function (v) {
var videoKbps = session.recordDefault;
if (session.recordLocal !== false) {
videoKbps = session.recordLocal;
}
if (document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']")) {
recordLocalVideoToggle(true);
} else if (v.stopWriter || v.recording) {
} else if (v.startWriter) {
v.startWriter();
} else {
recordLocalVideo(null, videoKbps, v);
}
},
2000,
session.videoElement
);
}
log("session.seeding: " + session.seeding);
if (session.seeding) {
setTimeout(function () {
if (session.meshcast2) {
meshcast2();
} else if (session.meshcast) {
meshcast();
} else if (session.whipOutput) {
whipOut();
} else if (session.whepHost) {
whepOut();
}
}, 1000);
return;
}
if (session.meshcast2) {
meshcast2();
} else 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 = "
Statistics
";
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.meshcast2) {
await meshcast2();
} else if (session.whipOutput) { // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024
whipOut();
} else if (session.meshcast) {
await meshcast();
} else 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);
}
}
if (session.autoEnd) {
log("Auto-end timer started: " + session.autoEnd + "ms");
// Create countdown display
const countdownDiv = document.createElement("div");
countdownDiv.id = "autoEndCountdown";
countdownDiv.style.cssText = "position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 10px 15px; border-radius: 5px; font-size: 16px; z-index: 9999; display: flex; align-items: center; gap: 8px;";
countdownDiv.innerHTML = '⏱️--:--';
document.body.appendChild(countdownDiv);
// Update countdown every second
let remainingTime = session.autoEnd;
const updateCountdown = () => {
const minutes = Math.floor(remainingTime / 60000);
const seconds = Math.floor((remainingTime % 60000) / 1000);
document.getElementById("autoEndTime").textContent =
String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
remainingTime -= 1000;
if (remainingTime < 0) {
clearInterval(session.autoEndInterval);
}
};
updateCountdown(); // Initial update
session.autoEndInterval = setInterval(updateCountdown, 1000);
// Set timer to end stream
session.autoEndTimer = setTimeout(() => {
log("Auto-end timer expired, ending stream");
clearInterval(session.autoEndInterval);
session.hangup();
}, session.autoEnd);
}
};
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 this guide for an alternative method to do so.", false, false);
} else if (session.mobile) {
warnUser("Sorry, your browser does not support screen-sharing.\n\nThe Android native app 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.meshcast2) {
await meshcast2();
} else if (session.whipOutput) { // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024
whipOut();
} else if (session.meshcast) {
await meshcast();
} else 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: " + fileinfo.name + " Do you trust them? ";
} else if (fileinfo.status === 1) {
data.msg = " has a shared a file with you: " + fileinfo.name + " ";
} else if (fileinfo.status === 2) {
data.msg = " has a shared a file with you: " + fileinfo.name + " ";
} else if (fileinfo.status === 3) {
data.msg = " has a shared a file with you: " + fileinfo.name + " ";
transferList[idx].status = 6;
} else if (fileinfo.status === 4) {
data.msg = " has a shared a file with you: " + fileinfo.name + " ";
} else if (fileinfo.status === 5) {
data.msg = " has a shared a file with you: " + fileinfo.name + " ";
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 = "" + data.label + "";
} else {
data.label = "" + data.label + "";
}
} else if (director) {
data.label = "Director";
} else {
const identifier = getPeerDisplayName(fileinfo.UUID, false);
if (identifier) {
data.label = "" + identifier + "";
} 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("legal").classList.remove("hidden");
//getById("helpbutton").style.display = "inherit";
//getById("reportbutton").style.display = "";
} else {
getById("controlButtons").classList.add("hidden");
// getById("legal").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 = 'This is your public audience link ';
}
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";
}
if (session.whipOutput) {
getById("head3").classList.add("hidden");
getById("head3a").classList.add("hidden");
}
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();
}
if (session.framegrab && session.framegrabAudioRequested && session.pendingFramegrabAudioSettings) {
try {
const maybePromise = session.startFramegrabAudio(session.pendingFramegrabAudioSettings);
if (maybePromise && typeof maybePromise.then === "function") {
maybePromise.catch(errorlog);
}
} catch (err) {
errorlog(err);
}
}
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");
// getById("legal").classList.remove("hidden");
} else {
getById("controlButtons").classList.add("hidden");
// getById("legal").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.meshcast2) {
meshcast2();
} else if (session.whipOutput) {
whipOut();
} else if (session.meshcast) {
meshcast();
} else 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("legal").classList.remove("hidden");
//getById("helpbutton").style.display = "inherit";
//getById("reportbutton").style.display = "";
} else {
getById("controlButtons").classList.add("hidden");
// getById("legal").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!
}
if (session.framegrab && session.framegrabAudioRequested && session.pendingFramegrabAudioSettings) {
try {
const maybePromise = session.startFramegrabAudio(session.pendingFramegrabAudioSettings);
if (maybePromise && typeof maybePromise.then === "function") {
maybePromise.catch(errorlog);
}
} catch (err) {
errorlog(err);
}
}
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.meshcast2) {
await meshcast2();
} else if (session.whipOutput) { // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024
whipOut();
} else if (session.meshcast) {
await meshcast();
} else 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: 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: 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;
}
// Cleanup audio nodes for a guest's mix-minus to prevent memory leaks
function cleanupMixMinusAudioNodes(uuid) {
if (!session.mixMinusState || !session.mixMinusState[uuid]) {
return;
}
var nodes = session.mixMinusState[uuid].audioNodes;
if (!nodes) {
return;
}
try {
// Disconnect all source nodes
if (nodes.sources) {
for (var i = 0; i < nodes.sources.length; i++) {
try {
nodes.sources[i].disconnect();
} catch (e) { }
}
nodes.sources = [];
}
// Disconnect all splitter nodes
if (nodes.splitters) {
for (var i = 0; i < nodes.splitters.length; i++) {
try {
nodes.splitters[i].disconnect();
} catch (e) { }
}
nodes.splitters = [];
}
// Disconnect merger
if (nodes.merger) {
try {
nodes.merger.disconnect();
} catch (e) { }
nodes.merger = null;
}
// Destination doesn't need explicit disconnect
nodes.destination = null;
} catch (e) {
warnlog("Error cleaning up mix-minus audio nodes: " + e);
}
}
// Director mix-minus: Creates a custom audio mix for a specific guest
// Includes all other guests' audio + director's audio, excluding the target guest's own audio
function createDirectorMixMinusForGuest(targetUUID) {
// Check if this guest has mix enabled (either via &mixminus or via UI)
// Allow if directorMixMinus is set OR if guest-specific state is enabled
if (!session.directorMixMinus && (!session.mixMinusState || !session.mixMinusState[targetUUID] || !session.mixMinusState[targetUUID].enabled)) {
return null;
}
if (!session.mixMinusState) {
session.mixMinusState = {};
}
if (!session.audioCtx) {
warnlog("Audio context not initialized for mix-minus");
return null;
}
// Initialize state for this guest if not exists
if (!session.mixMinusState[targetUUID]) {
initMixMinusStateForGuest(targetUUID);
}
var guestState = session.mixMinusState[targetUUID];
// Cleanup previous audio nodes before creating new ones (or if disabled)
cleanupMixMinusAudioNodes(targetUUID);
if (!guestState || !guestState.enabled) {
return null;
}
// Ensure audioNodes object exists
if (!guestState.audioNodes) {
guestState.audioNodes = {
merger: null,
destination: null,
sources: [],
splitters: []
};
}
// Create channel merger based on stereo setting
var merger;
try {
if (session.stereo === false) {
merger = session.audioCtx.createChannelMerger(1);
} else {
merger = session.audioCtx.createChannelMerger(2);
}
guestState.audioNodes.merger = merger;
} catch (e) {
errorlog("Failed to create audio merger for mix-minus: " + e);
return null;
}
// Helper function to connect audio track to merger
function connectTrackToMerger(track) {
try {
var tempStream = createMediaStream();
tempStream.addTrack(track);
var trackStream = session.audioCtx.createMediaStreamSource(tempStream);
guestState.audioNodes.sources.push(trackStream);
if (session.stereo !== false) {
var splitter = session.audioCtx.createChannelSplitter(2);
guestState.audioNodes.splitters.push(splitter);
trackStream.connect(splitter);
splitter.connect(merger, 0, 0);
try {
splitter.connect(merger, 1, 1);
} catch (e) {
errorlog(e);
try {
splitter.connect(merger, 0, 1);
} catch (e) {
errorlog(e);
}
}
} else {
trackStream.connect(merger, 0, 0);
}
} catch (e) {
errorlog(e);
}
}
// Add director's processed audio mix (if enabled)
if (guestState.useDirectorMix !== false && session.videoElement && session.videoElement.srcObject) {
var mixTracks = session.videoElement.srcObject.getAudioTracks();
for (var i = 0; i < mixTracks.length; i++) {
connectTrackToMerger(mixTracks[i]);
}
}
// Add raw input devices (if any are enabled)
if (guestState.rawDevices && session.streamSrc) {
var inputTracks = session.streamSrc.getAudioTracks();
for (var i = 0; i < inputTracks.length; i++) {
var track = inputTracks[i];
var deviceId = track.getSettings().deviceId || track.id;
if (guestState.rawDevices[deviceId] === true) {
connectTrackToMerger(track);
}
}
}
// Add all other guests' audio (from session.rpcs)
for (var UUID in session.rpcs) {
// Skip the target guest (they don't need to hear themselves)
if (UUID === targetUUID) {
continue;
}
// Check if this source is excluded for this guest
if (guestState.excludeSources && guestState.excludeSources.includes(UUID)) {
continue;
}
// If using include mode, check if source is explicitly included
if (guestState.includeSources && guestState.includeSources.length > 0) {
if (!guestState.includeSources.includes(UUID)) {
continue;
}
}
// Skip if rpcs entry doesn't exist or has no audio
if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].videoElement.srcObject) {
continue;
}
var guestTracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks();
for (var i = 0; i < guestTracks.length; i++) {
connectTrackToMerger(guestTracks[i]);
}
}
// Create destination stream
var destination = session.audioCtx.createMediaStreamDestination();
merger.connect(destination);
guestState.audioNodes.destination = destination;
return destination.stream;
}
// Initialize mix-minus state for a guest
function initMixMinusStateForGuest(uuid) {
if (!session.mixMinusState) {
session.mixMinusState = {};
}
if (!session.mixMinusDefaults) {
session.mixMinusDefaults = {
allGuestsEnabled: true,
includeDirectorAudio: true,
includeAllGuests: session.directorMixMinus ? true : false // Only include guests by default if &mixminus is set
};
}
// Don't overwrite existing state (audio nodes may already be stored)
if (session.mixMinusState[uuid]) {
return;
}
session.mixMinusState[uuid] = {
enabled: session.mixMinusDefaults.allGuestsEnabled,
excludeSources: [],
includeSources: [],
// Director audio options
useDirectorMix: true, // Use processed WebAudio output (with effects)
rawDevices: {}, // { deviceId: true/false } for raw input devices
directorAudioDevices: {}, // Legacy - kept for backwards compatibility
// Audio node references for cleanup
audioNodes: {
merger: null,
destination: null,
sources: [], // MediaStreamSource nodes
splitters: [] // ChannelSplitter nodes
}
};
// Initialize raw input devices (from session.streamSrc) - disabled by default
if (session.streamSrc) {
var inputTracks = session.streamSrc.getAudioTracks();
for (var i = 0; i < inputTracks.length; i++) {
var deviceId = inputTracks[i].getSettings().deviceId || inputTracks[i].id;
session.mixMinusState[uuid].rawDevices[deviceId] = false; // Disabled by default
}
}
// Legacy: also track processed WebAudio output devices
if (session.videoElement && session.videoElement.srcObject) {
var directorTracks = session.videoElement.srcObject.getAudioTracks();
for (var i = 0; i < directorTracks.length; i++) {
var deviceId = directorTracks[i].getSettings().deviceId || directorTracks[i].id;
session.mixMinusState[uuid].directorAudioDevices[deviceId] = true;
}
}
// Without &mixminus, exclude other guests by default (director audio only)
// With &mixminus, include all guests by default (mix-minus behavior)
if (!session.mixMinusDefaults.includeAllGuests) {
// Add all current guests (except target) to excludeSources
for (var guestUUID in session.rpcs) {
if (guestUUID !== uuid) {
session.mixMinusState[uuid].excludeSources.push(guestUUID);
}
}
}
}
// Toggle mix-minus enabled/disabled for a specific guest
function toggleMixMinusForGuest(uuid) {
if (!session.mixMinusState) {
session.mixMinusState = {};
}
if (!session.mixMinusState[uuid]) {
initMixMinusStateForGuest(uuid);
}
session.mixMinusState[uuid].enabled = !session.mixMinusState[uuid].enabled;
updateMixMinusForGuest(uuid);
return session.mixMinusState[uuid].enabled;
}
// Toggle a specific audio source in the mix for a guest
function toggleSourceInMixForGuest(sourceUUID, targetUUID) {
if (!session.mixMinusState) {
session.mixMinusState = {};
}
if (!session.mixMinusState[targetUUID]) {
initMixMinusStateForGuest(targetUUID);
}
var state = session.mixMinusState[targetUUID];
var idx = state.excludeSources.indexOf(sourceUUID);
if (idx > -1) {
state.excludeSources.splice(idx, 1); // Remove from exclude list (enable)
} else {
state.excludeSources.push(sourceUUID); // Add to exclude list (disable)
}
updateMixMinusForGuest(targetUUID);
return idx > -1; // Returns true if source is now enabled
}
// Toggle a director audio device in the mix for a guest
function toggleDirectorDeviceInMix(deviceId, targetUUID) {
if (!session.mixMinusState) {
session.mixMinusState = {};
}
if (!session.mixMinusState[targetUUID]) {
initMixMinusStateForGuest(targetUUID);
}
var state = session.mixMinusState[targetUUID];
state.directorAudioDevices[deviceId] = !state.directorAudioDevices[deviceId];
updateMixMinusForGuest(targetUUID);
return state.directorAudioDevices[deviceId];
}
// Set mix-minus state for all guests
function setMixMinusForAll(enabled) {
if (!session.mixMinusDefaults) {
session.mixMinusDefaults = {
allGuestsEnabled: enabled,
includeDirectorAudio: true,
includeAllGuests: true
};
} else {
session.mixMinusDefaults.allGuestsEnabled = enabled;
}
if (!session.mixMinusState) {
session.mixMinusState = {};
return;
}
for (var uuid in session.mixMinusState) {
session.mixMinusState[uuid].enabled = enabled;
updateMixMinusForGuest(uuid);
}
}
// Update/rebuild the mix-minus stream for a guest
// This should replace the audio track being sent to the guest
function updateMixMinusForGuest(uuid) {
if (!session.pcs[uuid]) {
return;
}
var mixStream = createDirectorMixMinusForGuest(uuid);
if (!mixStream) {
return;
}
var mixTracks = mixStream.getAudioTracks();
if (!mixTracks.length) {
return;
}
// Replace ALL audio tracks being sent to this guest
try {
var senders = session.pcs[uuid].getSenders();
var replacedCount = 0;
for (var i = 0; i < senders.length; i++) {
if (senders[i].track && senders[i].track.kind === "audio") {
senders[i].replaceTrack(mixTracks[0]);
replacedCount++;
}
}
if (replacedCount > 0) {
log("Updated mix-minus audio for guest: " + uuid + " (replaced " + replacedCount + " audio track(s))");
}
} catch (e) {
errorlog("Error updating mix-minus for guest " + uuid + ": " + e);
}
}
// Called when a new guest joins - initialize their mix-minus state and send mix
function onGuestJoinedMixMinus(uuid) {
if (!session.directorMixMinus) {
return;
}
initMixMinusStateForGuest(uuid);
// Also update existing guests' mixes to include the new guest
for (var existingUUID in session.mixMinusState) {
if (existingUUID !== uuid && session.mixMinusState[existingUUID].enabled) {
updateMixMinusForGuest(existingUUID);
}
}
}
// Called when a guest leaves - cleanup and update other guests' mixes
function onGuestLeftMixMinus(uuid) {
// Allow cleanup if directorMixMinus is set OR if there's any mix state
if (!session.directorMixMinus && !session.mixMinusState) {
return;
}
// Cleanup audio nodes before removing state
cleanupMixMinusAudioNodes(uuid);
// Remove from state
delete session.mixMinusState[uuid];
// Remove from exclude/include lists of other guests
for (var otherUUID in session.mixMinusState) {
var state = session.mixMinusState[otherUUID];
var idx = state.excludeSources.indexOf(uuid);
if (idx > -1) {
state.excludeSources.splice(idx, 1);
}
idx = state.includeSources.indexOf(uuid);
if (idx > -1) {
state.includeSources.splice(idx, 1);
}
// Update their mix since a source left
updateMixMinusForGuest(otherUUID);
}
}
// UI handler for mix-minus toggle button in director panel
function directToggleMixMinus(ele, event) {
if (!session.directorMixMinus) {
warnUser("Mix-minus not enabled. Add &mixminus to your director URL.");
return;
}
var UUID = ele.dataset.UUID;
if (!UUID) {
// Try to find UUID from parent element
try {
UUID = ele.closest("[data-UUID]").dataset.UUID;
} catch (e) {
errorlog("Could not find guest UUID for mix-minus toggle");
return;
}
}
var enabled = toggleMixMinusForGuest(UUID);
// Update button appearance
if (enabled) {
ele.classList.add("pressed");
ele.title = "Mix-minus enabled - this guest hears all other audio";
} else {
ele.classList.remove("pressed");
ele.title = "Mix-minus disabled for this guest";
}
log("Mix-minus for " + UUID + " is now: " + (enabled ? "enabled" : "disabled"));
}
// Global variable to track open dropdown
var activeMixDropdown = null;
// Toggle mix dropdown visibility and populate sources
function toggleMixDropdown(UUID, buttonEle, event) {
if (event) {
event.stopPropagation();
}
// Find or get UUID from button
if (!UUID && buttonEle) {
UUID = buttonEle.dataset.UUID;
if (!UUID) {
try {
UUID = buttonEle.closest("[data-UUID]").dataset.UUID;
} catch (e) {
errorlog("Could not find guest UUID for mix dropdown");
return;
}
}
}
// Find the dropdown container (sibling to PGM/Mic row)
var container = buttonEle.closest(".row").nextElementSibling;
if (!container || !container.classList.contains("mix-dropdown-container")) {
errorlog("Could not find mix dropdown container");
return;
}
var dropdown = container.querySelector(".mix-dropdown");
if (!dropdown) {
errorlog("Could not find mix dropdown element");
return;
}
// Close any other open dropdown
if (activeMixDropdown && activeMixDropdown !== dropdown) {
activeMixDropdown.style.display = "none";
activeMixDropdown.closest(".mix-dropdown-container").style.display = "none";
}
// Toggle visibility
if (dropdown.style.display === "none" || dropdown.style.display === "") {
// Initialize state if needed
if (!session.mixMinusState) {
session.mixMinusState = {};
}
if (!session.mixMinusState[UUID]) {
initMixMinusStateForGuest(UUID);
}
// Enable mix-minus for this guest if not already
if (!session.mixMinusState[UUID].enabled) {
session.mixMinusState[UUID].enabled = true;
updateMixMinusForGuest(UUID);
}
// Populate and show dropdown
populateMixDropdown(UUID, dropdown);
container.style.display = "block";
dropdown.style.display = "block";
activeMixDropdown = dropdown;
buttonEle.classList.add("pressed");
// Add click outside listener to close dropdown
setTimeout(function() {
document.addEventListener("click", closeMixDropdownOnClickOutside);
}, 10);
} else {
// Hide dropdown
dropdown.style.display = "none";
container.style.display = "none";
activeMixDropdown = null;
buttonEle.classList.remove("pressed");
document.removeEventListener("click", closeMixDropdownOnClickOutside);
}
}
// Close dropdown when clicking outside
function closeMixDropdownOnClickOutside(event) {
if (activeMixDropdown && !activeMixDropdown.contains(event.target)) {
var container = activeMixDropdown.closest(".mix-dropdown-container");
activeMixDropdown.style.display = "none";
if (container) {
container.style.display = "none";
}
// Find and un-press the Mix button
var row = container ? container.previousElementSibling : null;
if (row) {
var mixBtn = row.querySelector('[data-action-type="custom-mix"]');
if (mixBtn) {
mixBtn.classList.remove("pressed");
}
}
activeMixDropdown = null;
document.removeEventListener("click", closeMixDropdownOnClickOutside);
}
}
// Populate dropdown with available audio sources
function populateMixDropdown(targetUUID, dropdown) {
if (!dropdown) {
return;
}
var html = '
Audio Sources
';
var state = session.mixMinusState[targetUUID];
// Section 1: Director Mix (processed WebAudio output with effects)
html += '
';
html += '
Director Mix
';
if (session.videoElement && session.videoElement.srcObject) {
var mixTracks = session.videoElement.srcObject.getAudioTracks();
if (mixTracks.length > 0) {
var checked = state.useDirectorMix !== false;
html += '
';
html += '';
html += '';
html += '
';
} else {
html += '
No director mix available
';
}
} else {
html += '
No director mix available
';
}
html += '
';
// Section 2: Director Input Devices (raw, unprocessed)
html += '
';
html += '
Director Input Devices
';
if (session.streamSrc) {
var inputTracks = session.streamSrc.getAudioTracks();
if (inputTracks.length === 0) {
html += '
No input devices
';
} else {
for (var i = 0; i < inputTracks.length; i++) {
var track = inputTracks[i];
var deviceId = track.getSettings().deviceId || track.id;
var label = track.label || ("Mic " + (i + 1));
var checked = state.rawDevices && state.rawDevices[deviceId] === true;
html += '
';
html += '';
html += '';
html += '
';
}
}
} else {
html += '
No input devices
';
}
html += '
';
// Other guests section
html += '
';
html += '
Guests
';
var guestCount = 0;
for (var UUID in session.rpcs) {
// Skip the target guest (they don't hear themselves)
if (UUID === targetUUID) {
continue;
}
if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].videoElement.srcObject) {
continue;
}
var guestTracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks();
if (guestTracks.length === 0) {
continue;
}
guestCount++;
var guestLabel = session.rpcs[UUID].label || "Guest";
var streamID = session.rpcs[UUID].streamID || UUID.substring(0, 6);
var displayLabel = guestLabel + " - " + streamID;
// Check if source is excluded
var checked = state.excludeSources.indexOf(UUID) === -1;
html += '
';
html += '';
html += '';
html += '
';
}
if (guestCount === 0) {
html += '
No other guests
';
}
html += '
';
dropdown.innerHTML = html;
}
// Helper to sanitize labels for HTML display
function sanitizeLabel(str) {
if (!str) return "";
return str.replace(//g, ">").replace(/"/g, """);
}
// Toggle a source in the mix and update the mix
function toggleMixSource(targetUUID, sourceId, sourceType, checkbox) {
if (!session.mixMinusState || !session.mixMinusState[targetUUID]) {
return;
}
var state = session.mixMinusState[targetUUID];
if (sourceType === 'mix') {
// Toggle Director Mix (processed WebAudio output)
state.useDirectorMix = !state.useDirectorMix;
updateMixMinusForGuest(targetUUID);
} else if (sourceType === 'raw') {
// Toggle raw input device
if (!state.rawDevices) {
state.rawDevices = {};
}
state.rawDevices[sourceId] = !state.rawDevices[sourceId];
updateMixMinusForGuest(targetUUID);
} else if (sourceType === true) {
// Legacy: Toggle director audio device (backwards compatibility)
toggleDirectorDeviceInMix(sourceId, targetUUID);
} else {
// Toggle guest audio source (sourceType === false or undefined)
toggleSourceInMixForGuest(sourceId, targetUUID);
}
}
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;
trackSet.micPanning = session.micPanning !== false ? session.micPanning : false;
}
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 {
if (typeof session.gdrive.ensureInitialized === "function") {
await session.gdrive.ensureInitialized();
}
if (typeof session.gdrive.requestAccessToken === "function") {
session.gdrive.requestAccessToken();
}
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, autoRetry = false) {
if (!vid) {
return Promise.resolve(false);
}
try {
if (vid.webkitSupportsPresentationMode && typeof vid.webkitSetPresentationMode === "function") {
vid.webkitSetPresentationMode(vid.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture");
clearAutoPiPPrompt();
return Promise.resolve(true);
} else if (!document.pictureInPictureEnabled) {
return Promise.resolve(false);
}
var pipPromise = null;
if (document.pictureInPictureElement) {
if (document.pictureInPictureElement === vid) {
pipPromise = document.exitPictureInPicture();
} else {
pipPromise = document.exitPictureInPicture().catch(errorlog).then(() => vid.requestPictureInPicture());
}
} else {
pipPromise = vid.requestPictureInPicture();
}
if (!pipPromise || typeof pipPromise.then !== "function") {
clearAutoPiPPrompt();
return Promise.resolve(true);
}
return pipPromise
.then(() => {
clearAutoPiPPrompt();
return true;
})
.catch(err => {
if (autoRetry && err && (err.name === "NotAllowedError" || err.name === "InvalidStateError")) {
showAutoPiPPrompt(vid);
}
errorlog(err);
return false;
});
} catch (e) {
if (autoRetry && e && (e.name === "NotAllowedError" || e.name === "InvalidStateError")) {
showAutoPiPPrompt(vid);
}
errorlog(e);
return Promise.resolve(false);
}
}
function showAutoPiPPrompt(vid) {
if (!vid) {
return;
}
session.autoPiPPromptVideo = vid;
if (session.autoPiPPrompt) {
return;
}
var prompt = document.createElement("div");
prompt.id = "pipPrompt";
prompt.style.position = "fixed";
prompt.style.bottom = "16px";
prompt.style.right = "16px";
prompt.style.zIndex = "12000";
prompt.style.background = "rgba(20,20,24,0.95)";
prompt.style.border = "1px solid rgba(255,255,255,0.2)";
prompt.style.borderRadius = "8px";
prompt.style.padding = "12px";
prompt.style.maxWidth = "280px";
prompt.style.display = "flex";
prompt.style.flexDirection = "column";
prompt.style.gap = "8px";
prompt.style.boxShadow = "0 4px 12px rgba(0,0,0,0.35)";
var message = document.createElement("div");
message.innerText = "Click to enable picture-in-picture";
message.style.fontSize = "14px";
message.style.lineHeight = "18px";
var controls = document.createElement("div");
controls.style.display = "flex";
controls.style.justifyContent = "space-between";
controls.style.gap = "8px";
var confirmBtn = document.createElement("button");
confirmBtn.type = "button";
confirmBtn.innerText = "Open PiP";
confirmBtn.style.flex = "1";
confirmBtn.style.padding = "6px 10px";
confirmBtn.style.borderRadius = "6px";
confirmBtn.style.border = "1px solid rgba(255,255,255,0.2)";
confirmBtn.style.background = "var(--accent-color, #3a7afe)";
confirmBtn.style.color = "#fff";
confirmBtn.style.cursor = "pointer";
var dismissBtn = document.createElement("button");
dismissBtn.type = "button";
dismissBtn.innerText = "Not now";
dismissBtn.style.flex = "1";
dismissBtn.style.padding = "6px 10px";
dismissBtn.style.borderRadius = "6px";
dismissBtn.style.border = "1px solid rgba(255,255,255,0.2)";
dismissBtn.style.background = "rgba(255,255,255,0.08)";
dismissBtn.style.color = "#fff";
dismissBtn.style.cursor = "pointer";
confirmBtn.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
var target = session.autoPiPPromptVideo;
clearAutoPiPPrompt();
if (target) {
toggleSystemPip(target);
}
});
dismissBtn.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
clearAutoPiPPrompt();
});
controls.appendChild(confirmBtn);
controls.appendChild(dismissBtn);
prompt.appendChild(message);
prompt.appendChild(controls);
document.body.appendChild(prompt);
session.autoPiPPrompt = prompt;
}
function clearAutoPiPPrompt() {
if (session.autoPiPPrompt) {
try {
session.autoPiPPrompt.remove();
} catch (e) { }
session.autoPiPPrompt = false;
}
session.autoPiPPromptVideo = false;
}
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.micPanning !== false && n == 0) {
// Director-side control: Mic Panning (0..180, 90=center)
var label = document.createElement("label");
var i = "micPanning";
var div = document.createElement("div");
label.id = "label_" + i + "_" + UUID;
label.htmlFor = "constraints_" + i + "_" + UUID;
label.innerText = "Mic Pan:";
var input = document.createElement("input");
input.min = 0;
input.max = 180;
input.value = data.micPanning || 90;
input.title = "0=L, 90=C, 180=R";
input.type = "range";
input.dataset.keyname = i;
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";
var manualInput = document.createElement("input");
manualInput.type = "number";
manualInput.dataset.keyname = i;
manualInput.value = data.micPanning || 90;
manualInput.className = "manualInput";
manualInput.id = "constraints_manual_" + i + "_" + UUID;
manualInput.dataset.UUID = UUID;
manualInput.dataset.track = n;
manualInput.onchange = function (e) {
getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
requestChangeMicPanning(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track));
};
input.onchange = function (e) {
getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value);
requestChangeMicPanning(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();
requestChangeMicPanning(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 = "⚠ " + label.innerText;
}
var input = document.createElement("input");
if (i === "aspectRatio") {
input.max = 5;
input.min = 0.2;
input.step = 0.00001;
} else if (i === "exposureTime") {
input.min = data.cameraConstraints[i].min;
input.max = Math.min(data.cameraConstraints[i].max, 2000);
} 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);
}
// Mic Panning - local settings UI
if (session.micPanning !== false && ii == 0) {
if (getById("popupSelector_constraints_audio").style.display == "none") {
getById("advancedOptionsAudio").style.display = "inline-flex";
}
var label = document.createElement("label");
var i = "micPanning";
label.htmlFor = "constraints_" + i;
label.innerText = "Mic Pan:";
var input = document.createElement("input");
input.min = 0;
input.max = 180;
input.dataset.deviceid = track0.id;
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.micPanning !== false ? session.micPanning : 90;
label.innerHTML += " " + parseInt(input.value);
input.title = input.value + " (0=L, 90=C, 180=R)";
var manualInput = document.createElement("input");
manualInput.type = "number";
manualInput.dataset.keyname = i;
manualInput.dataset.deviceid = track0.id;
manualInput.dataset.labelname = label.innerHTML;
manualInput.value = parseInt(input.value);
manualInput.className = "manualInput";
manualInput.id = "label_" + i;
manualInput.onchange = function (e) {
getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value);
changeMicPanning(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);
changeMicPanning(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);
changeMicPanning(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 if (i === "exposureTime") {
input.min = parseFloat(session.cameraConstraints[i].min);
input.max = Math.min(parseFloat(session.cameraConstraints[i].max), 2000);
} 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.setRemoteAutofocus = async function (enabled) {
try {
var mode = enabled ? "continuous" : "manual";
await updateCameraConstraints("focusMode", mode, false, false, false);
session.focusDistance = false; // Reset stored focus distance
log("Autofocus set to: " + mode);
} catch (e) {
errorlog(e);
}
};
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) {
if (miconly) {
warnUser("We couldn't find a microphone.\n\nPlease ensure you have granted the microphone permissions.");
} else {
warnUser("We couldn't find a microphone or camera.\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.");
}
const safariPermissionBug = SafariVersion && SafariVersion > 18 && (iOS || iPad);
if (videoPermission === "granted" && !safariPermissionBug) {
constraint.video = false;
}
if (audioPermission === "granted" && !safariPermissionBug) {
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:\ndocs.vdo.ninja/guides/share-webcam-from-inside-obs.", false, false);
} else if (ChromiumVersion && !session.mobile) {
warnUser("
This invite link and OBS ingestion link are reusable.
\
Only one person may use a specific invite at a time.
\
The stream ID can be changed manually to something else; keep it unique and alphanumeric.
\
Nothing is stored server-side; links do not expire, nor is there anything to delete.
\
\
';
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 = '";
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 = '";
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 = '";
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";
}
// Handle submenu edge positioning
var submenus = menu.querySelectorAll('.context-menu__submenu');
submenus.forEach(function(submenu) {
submenu.classList.remove('context-menu__submenu--left');
var parentRect = submenu.parentElement.getBoundingClientRect();
var submenuWidth = 200; // Width defined in CSS
if (parentRect.right + submenuWidth > windowWidth) {
submenu.classList.add('context-menu__submenu--left');
}
});
}
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(session.mirrorExclude);
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;
}
if (taskItemInContext.dataset) {
taskItemInContext.dataset.rotated = taskItemInContext.rotated || 0;
}
updateVideoTransform(taskItemInContext);
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") === "PTZControls") {
// Requires MUTUAL remote: both local viewer AND remote peer must have &remote
if (session.remote && 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) {
togglePTZControls(taskItemInContext.dataset.UUID);
}
} else if (link.getAttribute("data-action") === "ResetAutofocus") {
// Requires MUTUAL remote: both local viewer AND remote peer must have &remote
if (session.remote && 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) {
session.requestAutofocusChange(true, taskItemInContext.dataset.UUID, session.remote);
}
} else if (link.getAttribute("data-action") === "RemoteControlsParent") {
return; // Don't close menu on submenu parent click
} 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&q=1";
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);
} else if (link.getAttribute("data-action") === "SendTip") {
var UUID = taskItemInContext.dataset.UUID;
if (UUID && session.rpcs[UUID] && session.rpcs[UUID].acceptsTips) {
if (typeof openTipModal === 'function') {
openTipModal(UUID);
}
} else if (session.pcs && session.pcs[UUID] && session.pcs[UUID].acceptsTips) {
if (typeof openTipModal === 'function') {
openTipModal(UUID);
}
}
}
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") === "RemoteControlsParent") {
// Show/hide the entire Remote Controls submenu
// Requires MUTUAL remote: both local viewer AND remote peer must have &remote
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
items[i].parentNode.classList.add("hidden");
} else if (session.remote && 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") === "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") === "SendTip") {
// Show tip option only if:
// 1. Video source accepts tips (publisher opt-in)
// 2. Viewer has opted in with &showtips (viewer opt-in)
// 3. Not in clean mode
// 4. Not in Electron
// 5. Performer has completed Stripe setup (validated)
var UUID = taskItemInContext.dataset.UUID;
var acceptsTips = false;
var tipId = null;
var tipServer = null;
if (UUID) {
if (session.rpcs && session.rpcs[UUID] && session.rpcs[UUID].acceptsTips) {
acceptsTips = true;
tipId = session.rpcs[UUID].tipId;
tipServer = session.rpcs[UUID].tipServer;
} else if (session.pcs && session.pcs[UUID] && session.pcs[UUID].acceptsTips) {
acceptsTips = true;
tipId = session.pcs[UUID].tipId;
tipServer = session.pcs[UUID].tipServer;
}
}
// Check if performer is validated (use cache if available)
var performerValid = false;
if (tipId) {
tipServer = tipServer || session.tipServer || "https://ninjabacker.com";
var cacheKey = tipServer + "/" + tipId;
performerValid = tipPerformerCache[cacheKey] === true;
}
if (acceptsTips && performerValid && session.showTips && !session.cleanOutput && navigator.userAgent.toLowerCase().indexOf(" electron/") === -1) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("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 = "
" + message + "
";
var menuState = 0;
var menuWidth;
var menuHeight;
var menuPosition;
var menuPositionX;
var menuPositionY;
var windowWidth;
var windowHeight;
if (menuState !== 1) {
menuState = 1;
menu.classList.add("context-menu--active");
}
menuWidth = menu.offsetWidth + 4;
menuHeight = menu.offsetHeight + 4;
windowWidth = window.innerWidth;
windowHeight = window.innerHeight;
if (windowWidth - posx < menuWidth) {
menu.style.left = windowWidth - menuWidth + "px";
} else {
menu.style.left = posx + "px";
}
if (windowHeight - posy < menuHeight) {
menu.style.top = windowHeight - menuHeight + "px";
} else {
menu.style.top = posy + "px";
}
function toggleMenuOff() {
if (menuState !== 0) {
menuState = 0;
menu.classList.remove("context-menu--active");
}
}
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 = "" + session.label + ": ";
} else {
label = "" + session.label + ": ";
}
} else if (session.director) {
label = "Director: ";
}
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, ">").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 '' + url + "" + 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;
}
}
const streamFallbackLabel = (!label && session.director && UUID && session.rpcs[UUID] && session.rpcs[UUID].streamID)
? getPeerDisplayName(UUID, false, false)
: false;
if (label) {
label = sanitizeLabel(label);
}
data.msg = msg;
if (label) {
data.label = label;
if (director) {
data.label = "" + data.label + ": ";
} else {
data.label = "" + data.label + ": ";
}
label = "" + label + ":"; // label+":";
} else if (director) {
data.label = "Director: ";
label = "Director:";
} else {
if (session.director) {
const fallback = streamFallbackLabel || "Someone";
data.label = "" + fallback + ": ";
if (streamFallbackLabel) {
label = "" + fallback + ":";
} else {
label = "";
}
} 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 = "" + label + " " + msg + " ";
textOverlay.appendChild(spanOverlay);
textOverlay.style.display = "block";
} else {
var spanOverlay = document.createElement("span");
spanOverlay.innerHTML = "" + label + " " + msg + " ";
textOverlay.appendChild(spanOverlay);
textOverlay.style.display = "block";
var showtime = msg.length * 200 + 3000;
if (showtime > 8000) {
showtime = 8000;
}
setTimeout(
function (ele) {
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 = "" + label + ": ";
} 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 + " ";
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");
if (session.chat) {
getById("chattoggle").classList.remove("pulsate");
}
const chatBody = document.getElementById("chatBody");
chatBody.innerHTML = "";
for (var i in messageList) {
var time = timeSince(messageList[i].time) || "";
time = " - " + time + "";
var msg = document.createElement("div");
var message = replaceURLs(messageList[i].msg);
if (messageList[i].type == "sent") {
msg.innerHTML = message + "" + time + "";
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 + "" + time + "";
msg.classList.add("inMessage");
} else if (messageList[i].type == "alert") {
msg.innerHTML = message + "" + time + "";
msg.classList.add("inMessage");
} else if (messageList[i].type == "tip") {
msg.innerHTML = message + "" + time + "";
msg.classList.add("tipMessage");
} else {
msg.innerHTML = message;
msg.classList.add("outMessage");
}
chatBody.appendChild(msg);
}
showDownloadLinks();
for (var i in msgTransferList) {
var time = timeSince(msgTransferList[i].time) || "";
time = " - " + time + "";
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 + "" + time + "";
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 + "" + time + "";
msg.classList.add("inMessage");
} else if (msgTransferList[i].type == "alert") {
msg.innerHTML = msgTransferList[i].msg + "" + time + "";
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=' LINKS (GUEST INVITES & SCENES)'
getById("showCustomizerButton1").style.backgroundColor = "";
getById("showCustomizerButton2").style.backgroundColor = "";
getById("showCustomizerButton3").style.backgroundColor = "";
getById("showCustomizerButton4").style.backgroundColor = "";
getById("showCustomizerButton1").style.boxShadow = "";
getById("showCustomizerButton2").style.boxShadow = "";
getById("showCustomizerButton3").style.boxShadow = "";
getById("showCustomizerButton4").style.boxShadow = "";
if (getById("customizeLinks" + arg).style.display != "none") {
getById("customizeLinks").style.display = "none";
getById("customizeLinks" + arg).style.display = "none";
} 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");
if (
typeof google === "undefined" ||
!google.accounts ||
!google.accounts.oauth2
) {
const gisError = new Error("Google Identity Services failed to load.");
errorlog(gisError);
if (!session.cleanOutput) {
warnUser("Google sign-in was blocked. Allow accounts.google.com and try again.", 8000);
}
if (gdrive.promise && gdrive.promise.reject) {
gdrive.promise.reject(gisError);
}
return;
}
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, start upload immediately when possible
if (filename) {
setTimeout(async () => {
try {
if (session.gdrive && session.gdrive.accessToken) {
console.log("Using cached Google Drive token for upload");
gdrive.sessionUri = await gdrive.startResumableUpload(filename);
if (gdrive.sessionUri) {
uploadLoop();
return;
}
console.warn("Failed to reuse cached Drive token, requesting a new one...");
}
} catch (err) {
errorlog(err);
}
console.log("Requesting access token for immediate upload");
tokenClient.requestAccessToken();
}, 250); // 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";
}
}
const DROPBOX_APP_KEY_FALLBACK = "uwxixfldkii1xpt";
const DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
const DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
const DROPBOX_SCOPES = "files.content.write files.metadata.write";
const DROPBOX_SDK_URL = "https://cdnjs.cloudflare.com/ajax/libs/dropbox.js/10.34.0/Dropbox-sdk.min.js";
const DROPBOX_OAUTH_STORAGE_KEY = "dropboxOAuthTokens";
const DROPBOX_OAUTH_SESSION_KEY = "dropboxOAuthSession";
const DROPBOX_AUTH_MESSAGE_SOURCE = "vdoninja-dropbox-auth";
const DROPBOX_ALLOWED_REDIRECT_ORIGINS = [
"https://vdo.ninja",
"https://dev.versus.cam",
"https://versus.cam",
"https://backup.vdo.ninja",
"https://obs.ninja",
"http://localhost:8080"
];
const DROPBOX_REFRESH_SKEW_MS = 120000;
var dropboxScriptPromise = null;
var dropboxInitPromise = null;
var dropboxAuthFlowPromise = null;
var dropboxAuthFlowResolver = null;
var dropboxAuthFlowRejecter = null;
var dropboxAuthWindow = null;
var dropboxAuthWindowMonitor = null;
function getDropboxAppKey() {
if (typeof session !== "undefined" && session.DROPBOX_APP_KEY) {
return session.DROPBOX_APP_KEY;
}
return DROPBOX_APP_KEY_FALLBACK;
}
function getDropboxRedirectUri() {
if (typeof window === "undefined" || !window.location || !window.location.origin) {
return DROPBOX_ALLOWED_REDIRECT_ORIGINS[0] + "/dropbox-auth.html";
}
var origin = window.location.origin.replace(/\/+$/, "");
if (DROPBOX_ALLOWED_REDIRECT_ORIGINS.indexOf(origin) === -1) {
return DROPBOX_ALLOWED_REDIRECT_ORIGINS[0] + "/dropbox-auth.html";
}
return origin + "/dropbox-auth.html";
}
function persistDropboxAuthSession(sessionData) {
try {
localStorage.setItem(DROPBOX_OAUTH_SESSION_KEY, JSON.stringify(sessionData));
} catch (e) { }
}
function clearDropboxAuthSession() {
try {
localStorage.removeItem(DROPBOX_OAUTH_SESSION_KEY);
} catch (e) { }
}
function getStoredDropboxOAuthTokens() {
if (typeof session !== "undefined" && session.dropboxOAuth) {
return session.dropboxOAuth;
}
try {
var raw = localStorage.getItem(DROPBOX_OAUTH_STORAGE_KEY);
if (!raw) {
return null;
}
var parsed = JSON.parse(raw);
if (parsed && typeof parsed === "object") {
if (typeof session !== "undefined") {
session.dropboxOAuth = parsed;
if (parsed.accessToken) {
session.dropboxAccessToken = parsed.accessToken;
}
}
return parsed;
}
} catch (e) { }
return null;
}
function persistDropboxOAuthTokens(record) {
if (!record || !record.accessToken) {
return;
}
var existing = getStoredDropboxOAuthTokens();
var normalized = {
accessToken: record.accessToken,
refreshToken: record.refreshToken || (existing && existing.refreshToken) || null,
expiresAt: record.expiresAt || (existing && existing.expiresAt) || 0,
scope: record.scope || (existing && existing.scope) || DROPBOX_SCOPES,
tokenType: record.tokenType || (existing && existing.tokenType) || "bearer"
};
try {
localStorage.setItem(DROPBOX_OAUTH_STORAGE_KEY, JSON.stringify(normalized));
} catch (e) { }
if (typeof session !== "undefined") {
session.dropboxOAuth = normalized;
session.dropboxAccessToken = normalized.accessToken;
}
}
function clearDropboxOAuthTokens() {
if (typeof session !== "undefined") {
session.dropboxOAuth = null;
}
try {
localStorage.removeItem(DROPBOX_OAUTH_STORAGE_KEY);
} catch (e) { }
}
function normalizeDropboxTokenResponse(response, fallbackRefreshToken = null) {
if (!response || typeof response !== "object" || !response.access_token) {
return null;
}
var expiresIn = 0;
if (response.expires_in) {
var parsed = parseInt(response.expires_in, 10);
if (!isNaN(parsed) && parsed > 0) {
expiresIn = parsed * 1000;
}
}
var expiresAt = expiresIn ? Date.now() + Math.max(0, expiresIn - DROPBOX_REFRESH_SKEW_MS) : 0;
return {
accessToken: response.access_token,
refreshToken: response.refresh_token || fallbackRefreshToken || null,
expiresAt: expiresAt,
scope: response.scope || DROPBOX_SCOPES,
tokenType: response.token_type || "bearer"
};
}
function dropboxTokenExpired(tokens) {
if (!tokens || !tokens.accessToken) {
return true;
}
if (!tokens.expiresAt) {
return false;
}
return Date.now() >= tokens.expiresAt;
}
async function refreshDropboxAccessToken(existingTokens) {
if (!existingTokens || !existingTokens.refreshToken) {
throw new Error("Dropbox refresh token missing.");
}
var body = new URLSearchParams();
body.set("grant_type", "refresh_token");
body.set("refresh_token", existingTokens.refreshToken);
body.set("client_id", getDropboxAppKey());
var response = await fetch(DROPBOX_TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: body.toString()
});
if (!response.ok) {
throw new Error("Failed to refresh Dropbox token.");
}
var data = await response.json();
var normalized = normalizeDropboxTokenResponse(data, existingTokens.refreshToken);
if (!normalized) {
throw new Error("Dropbox refresh response invalid.");
}
persistDropboxOAuthTokens(normalized);
return normalized;
}
async function ensureDropboxOAuthAccessToken({ interactive = false } = {}) {
var tokens = getStoredDropboxOAuthTokens();
if (tokens && !dropboxTokenExpired(tokens)) {
return tokens;
}
if (tokens && tokens.refreshToken) {
try {
return await refreshDropboxAccessToken(tokens);
} catch (e) {
errorlog(e);
clearDropboxOAuthTokens();
clearDropboxAuthSession();
tokens = null;
}
}
if (!interactive) {
return null;
}
return beginDropboxOAuthFlow();
}
function generateRandomString(length = 64) {
var charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
var result = "";
if (typeof window !== "undefined" && window.crypto && window.crypto.getRandomValues) {
var values = new Uint32Array(length);
window.crypto.getRandomValues(values);
for (var i = 0; i < length; i++) {
result += charset[values[i] % charset.length];
}
} else {
for (var j = 0; j < length; j++) {
result += charset[Math.floor(Math.random() * charset.length)];
}
}
return result;
}
function base64UrlEncode(arrayBuffer) {
var bytes = new Uint8Array(arrayBuffer);
var binary = "";
for (var i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
async function createDropboxPkcePair() {
var verifier = generateRandomString(64);
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle && typeof TextEncoder !== "undefined") {
try {
var data = new TextEncoder().encode(verifier);
var digest = await window.crypto.subtle.digest("SHA-256", data);
return { verifier: verifier, challenge: base64UrlEncode(digest), method: "S256" };
} catch (e) { }
}
return { verifier: verifier, challenge: verifier, method: "plain" };
}
function generateDropboxState() {
return generateRandomString(32);
}
async function beginDropboxOAuthFlow() {
if (dropboxAuthFlowPromise) {
return dropboxAuthFlowPromise;
}
dropboxAuthFlowPromise = new Promise(async (resolve, reject) => {
dropboxAuthFlowResolver = resolve;
dropboxAuthFlowRejecter = reject;
try {
var pkce = await createDropboxPkcePair();
var state = generateDropboxState();
var redirectUri = getDropboxRedirectUri();
var clientId = getDropboxAppKey();
clearDropboxAuthSession();
persistDropboxAuthSession({
verifier: pkce.verifier,
state: state,
redirectUri: redirectUri,
clientId: clientId,
scope: DROPBOX_SCOPES,
origin: typeof window !== "undefined" && window.location ? window.location.origin : "",
ts: Date.now()
});
var params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
code_challenge: pkce.challenge,
code_challenge_method: pkce.method,
token_access_type: "offline",
state: state
});
if (DROPBOX_SCOPES) {
params.set("scope", DROPBOX_SCOPES);
}
var authUrl = DROPBOX_AUTH_URL + "?" + params.toString();
dropboxAuthWindow = window.open(authUrl, "vdoninja-dropbox-auth", "width=600,height=720");
if (!dropboxAuthWindow) {
throw new Error("Dropbox authorization popup was blocked. Allow popups and try again.");
}
dropboxAuthWindowMonitor = setInterval(() => {
if (!dropboxAuthWindow || dropboxAuthWindow.closed) {
cleanupDropboxAuthFlow(new Error("Dropbox authorization window was closed."), true);
}
}, 750);
} catch (err) {
cleanupDropboxAuthFlow(err, true);
}
});
return dropboxAuthFlowPromise;
}
function cleanupDropboxAuthFlow(result, isError) {
if (dropboxAuthWindowMonitor) {
clearInterval(dropboxAuthWindowMonitor);
dropboxAuthWindowMonitor = null;
}
if (dropboxAuthWindow && !dropboxAuthWindow.closed) {
try {
dropboxAuthWindow.close();
} catch (e) { }
}
dropboxAuthWindow = null;
var resolver = dropboxAuthFlowResolver;
var rejecter = dropboxAuthFlowRejecter;
dropboxAuthFlowResolver = null;
dropboxAuthFlowRejecter = null;
dropboxAuthFlowPromise = null;
if (isError) {
if (typeof rejecter === "function") {
rejecter(result instanceof Error ? result : new Error(result || "Dropbox authorization failed"));
}
} else if (typeof resolver === "function") {
resolver(result);
}
}
function isAllowedDropboxAuthOrigin(origin) {
if (!origin) {
return false;
}
if (origin === window.location.origin) {
return true;
}
return DROPBOX_ALLOWED_REDIRECT_ORIGINS.indexOf(origin) !== -1;
}
function dropboxAuthMessageHandler(event) {
if (!event || !event.data || !isAllowedDropboxAuthOrigin(event.origin)) {
return;
}
var data = event.data;
if (!data || data.source !== DROPBOX_AUTH_MESSAGE_SOURCE) {
return;
}
if (data.type === "request-session" && event.source && typeof event.source.postMessage === "function") {
var sessionPayload = null;
try {
var rawSession = localStorage.getItem(DROPBOX_OAUTH_SESSION_KEY);
if (rawSession) {
sessionPayload = JSON.parse(rawSession);
}
} catch (e) {
sessionPayload = null;
}
if (sessionPayload && data.state && sessionPayload.state && sessionPayload.state !== data.state) {
sessionPayload = null;
}
try {
event.source.postMessage({ source: DROPBOX_AUTH_MESSAGE_SOURCE, type: "session", session: sessionPayload }, event.origin);
} catch (e) { }
return;
}
if (data.type === "tokens" && data.tokens) {
persistDropboxOAuthTokens(data.tokens);
clearDropboxAuthSession();
cleanupDropboxAuthFlow(data.tokens, false);
} else if (data.type === "error") {
if (data.clearTokens) {
clearDropboxOAuthTokens();
}
clearDropboxAuthSession();
cleanupDropboxAuthFlow(new Error(data.message || "Dropbox authorization failed"), true);
}
}
function streamSaverMessageHandler(event) {
if (!event || !event.data || !event.data.streamSaverError) {
return;
}
if (session && session.streamSaverFailed) {
return;
}
try {
session.streamSaverFailed = true;
} catch (e) {}
var reason = event.data.reason ? "\n\nDetails: " + event.data.reason : "";
promptAlt("Recording download setup failed. Local recordings may not save correctly." + reason + "\n\nEnable Service Workers or allow downloads, or use Google Drive recording instead.", false, false, false, 5000);
errorlog("StreamSaver failure: " + (event.data.reason || "unknown"));
warnlog("StreamSaver failure: " + (event.data.reason || "unknown"));
try {
if (session && session.directorUUID && session.directorList && session.directorList.length) {
var msg = {};
msg.recorder = -3;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
}
} catch (e) {}
}
if (typeof window !== "undefined") {
window.addEventListener("message", dropboxAuthMessageHandler, false);
window.addEventListener("message", streamSaverMessageHandler, false);
}
function getStoredDropboxToken() {
try {
return localStorage.getItem("dropboxAccessToken") || null;
} catch (e) {
return null;
}
}
function persistDropboxToken(token) {
if (!token) {
return;
}
try {
localStorage.setItem("dropboxAccessToken", token);
} catch (e) { }
}
function clearDropboxAuthState({ clearToken = false, clearOAuth = false } = {}) {
if (typeof session !== "undefined") {
session.dbx = false;
if (clearToken) {
session.dropboxAccessToken = null;
try {
localStorage.removeItem("dropboxAccessToken");
} catch (e) { }
}
}
if (clearOAuth) {
if (typeof session !== "undefined") {
session.dropboxAccessToken = null;
}
clearDropboxOAuthTokens();
clearDropboxAuthSession();
}
try {
localStorage.removeItem("dropboxSession");
} catch (e) { }
}
function dropboxErrorSuggestsReauth(error) {
if (!error) {
return false;
}
var summary = "";
if (error.error && error.error.error_summary) {
summary = error.error.error_summary;
} else if (error.error_summary) {
summary = error.error_summary;
}
if (summary && (summary.indexOf("expired_access_token") !== -1 || summary.indexOf("invalid_access_token") !== -1)) {
return true;
}
var status = error.status || (error.error && error.error.status) || false;
if (status && parseInt(status) === 401) {
return true;
}
return false;
}
function ensureDropboxSDKLoaded() {
if (window.Dropbox && window.Dropbox.Dropbox) {
return Promise.resolve();
}
if (dropboxScriptPromise) {
return dropboxScriptPromise;
}
dropboxScriptPromise = new Promise((resolve, reject) => {
var existing = document.querySelector("script[src='" + DROPBOX_SDK_URL + "']");
if (existing) {
existing.addEventListener("load", () => resolve(), { once: true });
existing.addEventListener("error", () => reject(new Error("Failed to load Dropbox SDK")), { once: true });
} else {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = DROPBOX_SDK_URL;
script.onload = () => resolve();
script.onerror = () => reject(new Error("Failed to load Dropbox SDK"));
document.head.appendChild(script);
}
}).catch(error => {
dropboxScriptPromise = null;
throw error;
});
return dropboxScriptPromise;
}
async function setupDropbox(accessToken = null, options = {}) {
if (typeof session === "undefined") {
return null;
}
var opts = typeof options === "object" && options !== null ? options : {};
var interactive = opts.interactive === true;
var forceReauth = opts.forceReauth === true;
var token = null;
var manualToken = null;
if (typeof accessToken === "string" && accessToken.trim().length) {
manualToken = accessToken.trim();
token = manualToken;
}
if (forceReauth && manualToken) {
forceReauth = false;
}
if (forceReauth) {
clearDropboxOAuthTokens();
if (typeof session !== "undefined") {
session.dropboxOAuth = null;
}
}
var preferOAuth = !manualToken && (forceReauth || (interactive && Boolean(session.dropboxOAuth || getStoredDropboxOAuthTokens())));
var oauthTokens = null;
if (!forceReauth) {
oauthTokens = session.dropboxOAuth || getStoredDropboxOAuthTokens();
}
if (oauthTokens && dropboxTokenExpired(oauthTokens)) {
try {
oauthTokens = await refreshDropboxAccessToken(oauthTokens);
} catch (e) {
errorlog(e);
clearDropboxOAuthTokens();
oauthTokens = null;
}
}
if (oauthTokens && oauthTokens.accessToken) {
session.dropboxOAuth = oauthTokens;
}
if (!token && oauthTokens && oauthTokens.accessToken) {
token = oauthTokens.accessToken;
}
if (!token) {
var legacySessionToken = session.dropboxAccessToken || null;
if (legacySessionToken && (!preferOAuth || (oauthTokens && oauthTokens.accessToken === legacySessionToken))) {
token = legacySessionToken;
}
}
if (!token && !preferOAuth) {
var paramToken = typeof urlParams !== "undefined" && urlParams.get("dropbox");
token = paramToken || getStoredDropboxToken();
}
if (forceReauth) {
token = null;
}
if (!token) {
try {
var oauthResponse = await ensureDropboxOAuthAccessToken({ interactive: interactive || preferOAuth });
if (oauthResponse && oauthResponse.accessToken) {
token = oauthResponse.accessToken;
}
} catch (e) {
throw e;
}
}
if (!token) {
return null;
}
if (!forceReauth && session.dbx && session.dropboxAccessToken === token) {
return session.dbx;
}
session.dropboxAccessToken = token;
if (manualToken && opts.persist !== false) {
persistDropboxToken(token);
}
if (dropboxInitPromise) {
return dropboxInitPromise;
}
dropboxInitPromise = (async currentToken => {
await ensureDropboxSDKLoaded();
if (!window.Dropbox || !window.Dropbox.Dropbox) {
throw new Error("Dropbox SDK unavailable");
}
var client = new Dropbox.Dropbox({ accessToken: currentToken });
session.dbx = client;
session.dropboxAccessToken = currentToken;
resumeDropbox();
return client;
})(token)
.catch(error => {
var needsReauth = dropboxErrorSuggestsReauth(error);
clearDropboxAuthState({ clearToken: needsReauth, clearOAuth: needsReauth });
throw error;
})
.finally(() => {
dropboxInitPromise = null;
});
return dropboxInitPromise;
}
if (typeof window !== "undefined") {
window.setupDropbox = setupDropbox;
}
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 && typeof setupDropbox === "function") {
try {
await setupDropbox();
} catch (e) {
errorlog(e);
}
}
if (!session.dbx) {
if (session.directorUUID) {
var failMsg = {};
failMsg.dropbox = -2;
for (var di = 0; di < session.directorList.length; di++) {
failMsg.UUID = session.directorList[di];
session.sendPeers(failMsg, failMsg.UUID);
}
}
return;
}
var main;
try {
main = await session.dbx.filesUploadSessionStart({ close: false });
} catch (e) {
errorlog(e);
if (!session.cleanOutput) {
warnUser("Dropbox failed to initialize.\n\nAre your credentials valid? Tokens may expire after a few hours.", 8000);
}
if (dropboxErrorSuggestsReauth(e)) {
clearDropboxAuthState({ clearToken: true, clearOAuth: true });
}
return;
}
var sessionId = main.result.session_id;
var offset = 0;
var chunkCounter = 0;
var DBXqueue = [];
var resolverQueue = [];
var uploadActive = false;
log(main);
main.vdo = { filename: filename, offset: offset };
var persisted = localStorage.getItem("dropboxSession");
if (persisted) {
try {
persisted = JSON.parse(persisted);
} catch (e) {
errorlog(e);
persisted = [];
}
persisted.push(main);
} else {
persisted = [main];
}
localStorage.setItem("dropboxSession", JSON.stringify(persisted));
if (session.directorUUID) {
var initMsg = {};
initMsg.dropbox = -1;
for (var ii = 0; ii < session.directorList.length; ii++) {
initMsg.UUID = session.directorList[ii];
session.sendPeers(initMsg, initMsg.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) + "%";
}
}
function notifyDropboxQueueSize() {
if (!session.directorUUID) {
return;
}
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);
}
}
function resolveNext(value) {
var resolver = resolverQueue.shift();
if (resolver && resolver.resolve) {
resolver.resolve(value);
}
}
function rejectNext(error) {
var resolver = resolverQueue.shift();
if (resolver && resolver.reject) {
resolver.reject(error);
}
}
function rejectPending(error) {
while (resolverQueue.length) {
var pending = resolverQueue.shift();
if (pending && pending.reject) {
pending.reject(error);
}
}
}
function handleDropboxUploadFailure(error) {
errorlog(error);
if (dropboxErrorSuggestsReauth(error)) {
clearDropboxAuthState({ clearToken: true, clearOAuth: true });
}
if (!session.cleanOutput) {
warnUser("Dropbox upload failed. Please verify your token and try again.", 8000);
}
}
async function processQueue() {
if (uploadActive) {
return;
}
uploadActive = true;
while (DBXqueue.length) {
var current = DBXqueue[0];
try {
if (current === false) {
await session.dbx.filesUploadSessionFinish({ cursor: { session_id: sessionId, offset: offset }, commit: { path: "/" + filename } });
DBXqueue.shift();
resolveNext(true);
var sessionData = localStorage.getItem("dropboxSession");
if (sessionData) {
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;
notifyDropboxQueueSize();
finishedChunksUploaded();
} else {
await session.dbx.filesUploadSessionAppendV2({ cursor: { session_id: sessionId, offset: offset }, close: false, contents: current });
offset += current.size;
main.vdo.offset = offset;
var sessionData = localStorage.getItem("dropboxSession");
if (sessionData) {
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();
resolveNext(true);
updateTotalChunksUploaded();
chunkCounter += 1;
notifyDropboxQueueSize();
}
} catch (error) {
rejectNext(error);
rejectPending(error);
DBXqueue = [];
uploadActive = false;
handleDropboxUploadFailure(error);
return;
}
}
uploadActive = false;
}
function enqueueChunk(chunk) {
return new Promise((resolve, reject) => {
if (!sessionId) {
reject(new Error("Dropbox session inactive"));
return;
}
if (DBXqueue.length && DBXqueue[DBXqueue.length - 1] === false) {
resolve();
return;
}
DBXqueue.push(chunk);
resolverQueue.push({ resolve: resolve, reject: reject });
updateTotalChunksRecorded();
notifyDropboxQueueSize();
processQueue();
});
}
function uploadChunk(chunk) {
var promise = enqueueChunk(chunk);
promise.catch(() => { });
return promise;
}
return uploadChunk;
}
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 = " Download";
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 = ' record local';
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";
}
}
if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") {
try {
window.dispatchEvent(
new CustomEvent("vdoninja:gdrive-progress", {
detail: {
UUID: UUID,
gdrive: gdrive,
screen: screen
}
})
);
} catch (e) { }
}
}
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 = ' 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 = ' 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 = ' stopping...';
} else if (time == -1) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
elements[0].innerHTML = ' Record Remote';
} else {
var minutes = Math.floor(time / 60);
var seconds = time - minutes * 60;
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' ' + 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 = ' ARMED';
//
if (time == -3) {
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' ARMED';
elements[0].style.backgroundColor = "#BF3F3F";
} else if (time == -2) {
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' stopping...';
elements[0].style.backgroundColor = "";
} else if (time == -1) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
elements[0].innerHTML = ' Record Local';
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 = ' ' + minutes + "m : " + zpadTime(seconds) + "s";
elements[0].style.backgroundColor = "";
}
}
}
async function recordLocalVideoToggle(startonly = false) {
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 = '';
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 = ' Record';
}
}
return true;
} else if (!startonly) {
if ("recording" in session.videoElement) {
var res = await recordLocalVideo("stop");
log(res);
}
ele.dataset.state = "0";
ele.style.backgroundColor = "";
ele.innerHTML = '';
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 = ' Record';
}
}
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));
}
function setupExternalSensorBridge() {
if (session.externalSensorBridgeAttached) {
return;
}
session.externalSensorBridgeAttached = true;
log("External sensor bridge attached");
window.addEventListener("message", event => {
const payload = event.data;
if (!payload || !payload.sensors) {
return;
}
if (session.externalSensorOrigin && event.origin && event.origin !== "null" && event.origin !== session.externalSensorOrigin) {
log("Sensor message rejected due to origin mismatch: " + event.origin);
return;
}
session.sensors = session.sensors || {};
session.sensors.data = session.sensors.data || {};
try {
Object.assign(session.sensors.data, payload.sensors);
} catch (e) {}
try {
session.sendMessage({ sensors: payload.sensors });
} catch (e) {
errorlog(e);
}
});
}
//// 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 = '';
}
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 = '';
}
} 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 = '';
}
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 = '';
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 = '';
getById("recordLocalbutton").dataset.state = "2";
}
} else {
getById("recordLocalbutton").dataset.state = "0";
getById("recordLocalbutton").style.backgroundColor = "";
getById("recordLocalbutton").innerHTML = '';
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 = '';
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 = '';
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 = '';
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 = '';
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;
}
async function recordWindowCapture(bitrate = 6000) {
// Streamlined window/tab recording for scenes
// Captures the current browser tab and records to disk
//
// Customizable via URL parameters:
// &recordwindow=BITRATE - recording bitrate in kbps (default: 6000)
// &pcm - use PCM audio (lossless, larger files)
// &screensharefps=FPS - capture framerate (default: 60)
// &screensharequality=X - resolution: 4k, 2k, 1080p, 720p, etc.
// &width=W&height=H - custom resolution
if (session.recordWindowElement && session.recordWindowElement.recording) {
log("Window recording already in progress");
return;
}
try {
// Determine resolution from session parameters
var targetWidth = 1920;
var targetHeight = 1080;
if (session.screensharequality) {
var q = parseInt(session.screensharequality);
if (q === -2) { // 4k
targetWidth = 3840;
targetHeight = 2160;
} else if (q === -3) { // 2k/1440p
targetWidth = 2560;
targetHeight = 1440;
} else if (q === 1) { // 720p
targetWidth = 1280;
targetHeight = 720;
} else if (q === 2) { // 360p
targetWidth = 640;
targetHeight = 360;
}
}
if (session.width) {
targetWidth = parseInt(session.width) || targetWidth;
}
if (session.height) {
targetHeight = parseInt(session.height) || targetHeight;
}
// Determine framerate
var targetFps = 60;
if (session.screensharefps) {
targetFps = parseInt(session.screensharefps) || 60;
}
var constraints = {
video: {
frameRate: { ideal: targetFps },
width: { ideal: targetWidth },
height: { ideal: targetHeight },
cursor: "never"
},
audio: true,
preferCurrentTab: true,
selfBrowserSurface: "include",
surfaceSwitching: "exclude"
};
if (session.displaySurface) {
constraints.video.displaySurface = session.displaySurface;
}
if (session.suppressLocalAudioPlayback) {
constraints.audio = { suppressLocalAudioPlayback: true };
}
log("Starting window capture: " + targetWidth + "x" + targetHeight + "@" + targetFps + "fps");
var stream = await navigator.mediaDevices.getDisplayMedia(constraints);
// Create a temp video element to hold the capture
var video = document.createElement("video");
video.id = "recordWindowSource";
video.srcObject = stream;
video.muted = true;
video.autoplay = true;
video.playsInline = true;
video.style.display = "none";
document.body.appendChild(video);
session.recordWindowElement = video;
// Handle stream ending (user stops sharing)
stream.getVideoTracks()[0].onended = function() {
log("Window capture ended");
if (video.recording) {
recordLocalVideo("stop", false, video);
}
video.remove();
session.recordWindowElement = null;
// Reset button state if exists
var btn = document.getElementById("recordWindowButton");
if (btn) {
btn.innerHTML = "● Start Recording";
btn.title = "Record this scene to a local video file";
btn.style.background = "#d00";
btn.style.opacity = "1";
btn.dataset.recording = "0";
}
// Stop Go Live if active
if (session.goLivePC) {
try {
session.goLivePC.close();
} catch(e) {}
session.goLivePC = null;
}
var liveBtn = document.getElementById("goLiveButton");
if (liveBtn) {
liveBtn.innerHTML = "📡 Go Live";
liveBtn.title = "Stream to Twitch via WHIP (requires stream key)";
liveBtn.style.background = "#6441a5";
liveBtn.dataset.live = "0";
}
};
await video.play();
// Start recording using existing infrastructure
var usePCM = session.pcm || false;
var configureRecording = {
bitrate: bitrate,
usePCM: usePCM,
audioOnly: false
};
log("Starting window recording at " + bitrate + " kbps" + (usePCM ? " with PCM audio" : ""));
recordLocalVideo("start", configureRecording, video);
} catch (e) {
errorlog("Window capture failed: " + e);
if (session.recordWindowElement) {
session.recordWindowElement.remove();
session.recordWindowElement = null;
}
// Reset button state on error/cancel
var btn = document.getElementById("recordWindowButton");
if (btn) {
btn.innerHTML = "● Start Recording";
btn.style.background = "#d00";
btn.style.opacity = "1";
btn.dataset.recording = "0";
}
}
}
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) {
// FIX: Only replace if old track is dead (ended)
// This prevents audio clicks during normal operation
if (tracks[j].readyState === "ended") {
try {
session.rpcs[UUID].streamSrc.removeTrack(tracks[j]);
log("Replaced dead " + tracks[j].kind + " track");
} catch(e) { warnlog(e); }
} else {
// Old track is still live - skip duplicate as before
newTracks.splice(i, 1);
i--;
}
break;
}
}
}
}
var screenshare = false;
var screenshareParentOverride = null;
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;
}
}
}
if (typeof UUID === "string" && UUID.endsWith("_screen")) {
if (!screenshare) {
screenshare = true;
}
if (session.rpcs[UUID] && session.rpcs[UUID].realUUID) {
screenshareParentOverride = session.rpcs[UUID].realUUID;
} else {
screenshareParentOverride = UUID.slice(0, -7);
}
if (session.rpcs[UUID]) {
session.rpcs[UUID].screenShareState = true;
if (typeof session.rpcs[UUID].smallScreen === "undefined" || session.rpcs[UUID].smallScreen === null) {
session.rpcs[UUID].smallScreen = false;
}
}
}
if (screenshare) {
const parentUUID = screenshareParentOverride || (typeof UUID === "string" && UUID.endsWith("_screen") ? UUID.slice(0, -7) : UUID);
if (parentUUID && session.rpcs[parentUUID]) {
session.rpcs[parentUUID].screenShareState = true;
}
if (parentUUID && session.rpcs[parentUUID + "_screen"]) {
session.rpcs[parentUUID + "_screen"].screenShareState = true;
}
}
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) {
var targetUUID = screenshareParentOverride || UUID;
if (session.rpcs[targetUUID]) {
session.setupScreenShareAddon(newTracks, targetUUID);
} else {
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.
log("removetrack");
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);
// Update local label immediately and sync to co-directors
try {
session.rpcs[UUID].label = newlabel;
session.rpcs[UUID].labelSetByDirector = true; // Prevent info message from overwriting
// Push label update to co-directors via directorState sync
if (session.director && session.rpcs[UUID].streamID) {
var labelPayload = { directorState: {} };
labelPayload.directorState[session.rpcs[UUID].streamID] = getDetailedState(session.rpcs[UUID].streamID);
session.pushDirectorStateUpdate(labelPayload, "label-change");
}
} catch (e) { errorlog(e); }
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) {
try { getById("container_" + UUID).classList.add("directorBlue"); log("[ui] addDirectorBlue for UUID=" + UUID); } catch (e) { errorlog(e); }
}
function soloLinkGeneratorInit(UUID) {
document.querySelectorAll("container_" + UUID).forEach(ele => {
ele.querySelectorAll("[data-sololink]").forEach(ele2 => {
// value='" + soloLink + "' href='" + soloLink + "'/>" + soloLink + "
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('");
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 = ' ' + 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) {
// Normalize value to [-1, 1] where 0=center
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 {
// input 0..180 => -1..1
value = parseFloat(value / 90) - 1 || 0;
}
if (value < -1) value = -1;
if (value > 1) value = 1;
// Pre-pan gain trim to avoid clipping
var gainNode = session.audioCtx.createGain();
session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode = gainNode;
gainNode.gain.value = 1 - Math.abs(value) / 2;
source.connect(gainNode);
// Create panner with Safari fallback
var panNode;
try {
if (session.audioCtx.createStereoPanner) {
panNode = session.audioCtx.createStereoPanner();
session.rpcs[UUID].inboundAudioPipeline[trackid].panType = "stereo";
panNode.pan.value = value;
} else {
panNode = session.audioCtx.createPanner();
panNode.panningModel = "equalpower";
panNode.distanceModel = "inverse";
var x = value;
var z = 1 - Math.abs(value);
try {
if (typeof panNode.positionX !== "undefined") {
panNode.positionX.value = x;
panNode.positionY.value = 0;
panNode.positionZ.value = z;
} else if (panNode.setPosition) {
panNode.setPosition(x, 0, z);
}
} catch (e) { }
session.rpcs[UUID].inboundAudioPipeline[trackid].panType = "panner";
}
} catch (e) {
warnlog("Stereo panning node creation failed; bypassing");
return gainNode;
}
session.rpcs[UUID].inboundAudioPipeline[trackid].panNode = panNode;
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]) {
try {
if (session.rpcs[UUID].inboundAudioPipeline[trackid].panType === "stereo" && session.rpcs[UUID].inboundAudioPipeline[trackid].panNode.pan) {
session.rpcs[UUID].inboundAudioPipeline[trackid].panNode.pan.setValueAtTime(value, session.audioCtx.currentTime);
} else {
// Fallback panner
var x = value;
var z = 1 - Math.abs(value);
var pn = session.rpcs[UUID].inboundAudioPipeline[trackid].panNode;
if (typeof pn.positionX !== "undefined") {
pn.positionX.setValueAtTime(x, session.audioCtx.currentTime);
pn.positionY.setValueAtTime(0, session.audioCtx.currentTime);
pn.positionZ.setValueAtTime(z, session.audioCtx.currentTime);
} else if (pn.setPosition) {
pn.setPosition(x, 0, z);
}
}
} catch (e) { warnlog(e); }
}
if ("gainPanNode" in session.rpcs[UUID].inboundAudioPipeline[trackid] && session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode.gain) {
try { session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode.gain.setValueAtTime(1 - Math.abs(value) / 2, session.audioCtx.currentTime); } catch (e) { }
}
}
}
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.
This effect will not work", 4000, false);
return;
}
} else if (gpgpuSupport == "Google SwiftShader") {
if (!session.cleanOutput) {
warnUser("Hardware acceleration isn't detected.
Please enable it for this effect to work correctly.
Settings -> Advanced -> System -> Use hardware-accleration", 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.