/*
* 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);
function mergeFragmentParams(queryParams) {
// Merge fragment params (after #) with query params
// Fragment params come first AND take precedence for conflicts
var fragString = window.location.hash.slice(1); // remove leading #
if (!fragString) {
return queryParams; // no fragment, return as-is
}
fragString = fragString.replace(/\?\?/g, "?");
fragString = fragString.replace(/\?/g, "&");
fragString = fragString.replace(/\&/, "?");
var fragParams = new URLSearchParams(fragString);
// Build result: fragment params first, then query params (skip conflicts)
var result = new URLSearchParams();
for (const [key, value] of fragParams) {
result.set(key, value);
}
for (const [key, value] of queryParams) {
if (!result.has(key)) {
result.set(key, value);
}
}
return result;
}
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
var urlParams = mergeFragmentParams(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 = mergeFragmentParams(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 = mergeFragmentParams(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;
const memory = typeof navigator.deviceMemory === 'number' ? navigator.deviceMemory : 0;
if (isIntelMac()) {
if (cores < 6) { // yes. they are that bad.
return 2;
} else {
return 1;
}
}
if (session.mobile) {
if ((cores && cores < 4) || (memory && memory <= 2)) {
return 2;
} else if (cores >= 8 && (!memory || memory >= 6)) {
return 0;
} else if (cores >= 4) { // assume hardware encoded acceleration
return 1;
}
}
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 nativeGetUserMediaForTests = null;
var syntheticGetUserMediaEnabled = false;
var syntheticMediaStreamCounter = 0;
function parseSyntheticConstraintNumber(entry, fallback) {
if (typeof entry === "number" && Number.isFinite(entry)) {
return entry;
}
if (!entry || typeof entry !== "object") {
return fallback;
}
if (typeof entry.exact === "number" && Number.isFinite(entry.exact)) {
return entry.exact;
}
if (typeof entry.ideal === "number" && Number.isFinite(entry.ideal)) {
return entry.ideal;
}
if (typeof entry.max === "number" && Number.isFinite(entry.max)) {
return entry.max;
}
if (typeof entry.min === "number" && Number.isFinite(entry.min)) {
return entry.min;
}
return fallback;
}
function clampSyntheticSetting(value, minValue, maxValue, fallback) {
if (!Number.isFinite(value)) {
value = fallback;
}
if (!Number.isFinite(value)) {
value = minValue;
}
return Math.min(maxValue, Math.max(minValue, parseInt(value) || minValue));
}
function constraintsRequestMediaKind(constraints, key) {
if (!constraints || typeof constraints !== "object") {
return false;
}
if (!(key in constraints)) {
return false;
}
return constraints[key] !== false;
}
function createSyntheticTestMediaStream(constraints = {}) {
var wantsAudio = constraintsRequestMediaKind(constraints, "audio");
var wantsVideo = constraintsRequestMediaKind(constraints, "video");
if (!wantsAudio && !wantsVideo) {
if (!constraints || typeof constraints !== "object" || (!("audio" in constraints) && !("video" in constraints))) {
wantsAudio = session.testMediaAudio !== false;
wantsVideo = session.testMediaVideo !== false;
}
}
if (session.testMediaAudio === false) {
wantsAudio = false;
}
if (session.testMediaVideo === false) {
wantsVideo = false;
}
if (!wantsAudio && !wantsVideo) {
var noTracksErr = new Error("Synthetic test media disabled for both audio and video.");
noTracksErr.name = "NotFoundError";
throw noTracksErr;
}
syntheticMediaStreamCounter += 1;
var syntheticStreamID = syntheticMediaStreamCounter;
var stream = createMediaStream();
var cleanupTriggered = false;
var trackCount = 0;
var drawStopper = null;
var drawInterval = null;
var videoStream = null;
var audioContext = null;
var oscillator = null;
var gainNode = null;
var schedulerOscillator = null;
var schedulerGain = null;
var schedulerToken = 0;
var getSyntheticAudioContext = function () {
if (audioContext) {
return audioContext;
}
var AudioContextCtor = window.AudioContext || window.webkitAudioContext;
if (!AudioContextCtor) {
return null;
}
audioContext = new AudioContextCtor({ sampleRate: 48000 });
if (audioContext.state === "suspended") {
audioContext.resume().catch(function () {});
}
return audioContext;
};
var stopSyntheticVideoScheduler = function () {
schedulerToken += 1;
try {
if (schedulerOscillator) {
schedulerOscillator.onended = null;
schedulerOscillator.stop();
schedulerOscillator.disconnect();
schedulerOscillator = null;
}
} catch (e) {}
try {
if (schedulerGain) {
schedulerGain.disconnect();
schedulerGain = null;
}
} catch (e) {}
drawStopper = null;
};
var startSyntheticVideoScheduler = function (frameRate, drawCallback) {
var context = getSyntheticAudioContext();
if (!context || context.state !== "running") {
return false;
}
var schedulerId = ++schedulerToken;
var scheduleNextFrame = function (nextTime) {
if (cleanupTriggered || schedulerToken !== schedulerId) {
return;
}
schedulerOscillator = context.createOscillator();
schedulerGain = context.createGain();
schedulerGain.gain.value = 0;
schedulerOscillator.connect(schedulerGain);
schedulerGain.connect(context.destination);
schedulerOscillator.onended = function () {
try {
schedulerOscillator.disconnect();
} catch (e) {}
try {
schedulerGain.disconnect();
} catch (e) {}
schedulerOscillator = null;
schedulerGain = null;
if (cleanupTriggered || schedulerToken !== schedulerId) {
return;
}
drawCallback();
scheduleNextFrame(context.currentTime);
};
schedulerOscillator.start(nextTime);
schedulerOscillator.stop(nextTime + 1 / frameRate);
};
scheduleNextFrame(context.currentTime);
drawStopper = stopSyntheticVideoScheduler;
return true;
};
var cleanupSyntheticStream = function () {
if (cleanupTriggered) {
return;
}
cleanupTriggered = true;
if (drawStopper) {
drawStopper();
}
if (drawInterval) {
clearInterval(drawInterval);
drawInterval = null;
}
if (videoStream && videoStream.getTracks) {
videoStream.getTracks().forEach(function (track) {
try {
if (track.readyState !== "ended") {
track.stop();
}
} catch (e) {}
});
}
try {
if (oscillator) {
oscillator.onended = null;
oscillator.stop();
oscillator.disconnect();
oscillator = null;
}
} catch (e) {}
try {
if (gainNode) {
gainNode.disconnect();
gainNode = null;
}
} catch (e) {}
try {
if (audioContext && audioContext.state !== "closed") {
audioContext.close().catch(function () {});
}
} catch (e) {}
audioContext = null;
videoStream = null;
};
if (wantsVideo) {
var requestedVideo = constraints && typeof constraints.video === "object" ? constraints.video : {};
var targetWidth = clampSyntheticSetting(
parseSyntheticConstraintNumber(requestedVideo.width, session.testMediaWidth || 1280),
160,
3840,
session.testMediaWidth || 1280
);
var targetHeight = clampSyntheticSetting(
parseSyntheticConstraintNumber(requestedVideo.height, session.testMediaHeight || 720),
120,
2160,
session.testMediaHeight || 720
);
var targetFps = clampSyntheticSetting(
parseSyntheticConstraintNumber(requestedVideo.frameRate, session.testMediaFps || 30),
1,
60,
session.testMediaFps || 30
);
var canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
var ctx = canvas.getContext("2d");
var frameIndex = 0;
var barCount = 8;
var barWidth = Math.ceil(targetWidth / barCount);
var drawSyntheticFrame = function () {
if (!ctx) {
return;
}
var now = new Date();
ctx.fillStyle = "#101820";
ctx.fillRect(0, 0, targetWidth, targetHeight);
for (var b = 0; b < barCount; b++) {
var hue = (frameIndex * 2 + b * 40) % 360;
ctx.fillStyle = "hsl(" + hue + ", 70%, 50%)";
ctx.fillRect(b * barWidth, 0, barWidth, Math.floor(targetHeight * 0.7));
}
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(0, Math.floor(targetHeight * 0.7), targetWidth, targetHeight - Math.floor(targetHeight * 0.7));
ctx.fillStyle = "#ffffff";
ctx.font = "bold " + Math.max(20, Math.floor(targetHeight / 18)) + "px monospace";
ctx.fillText("VDO.NINJA TEST MEDIA", 24, Math.floor(targetHeight * 0.77));
ctx.font = "normal " + Math.max(16, Math.floor(targetHeight / 24)) + "px monospace";
ctx.fillText(now.toISOString(), 24, Math.floor(targetHeight * 0.85));
ctx.fillText("stream " + syntheticStreamID + " | " + targetWidth + "x" + targetHeight + "@" + targetFps, 24, Math.floor(targetHeight * 0.92));
frameIndex += 1;
};
drawSyntheticFrame();
if (!startSyntheticVideoScheduler(targetFps, drawSyntheticFrame)) {
drawInterval = setInterval(drawSyntheticFrame, Math.max(16, Math.round(1000 / targetFps)));
drawStopper = function () {
if (drawInterval) {
clearInterval(drawInterval);
drawInterval = null;
}
};
warnlog("AudioContext unavailable; synthetic video frame pump may throttle in background tabs.");
}
if (typeof canvas.captureStream === "function") {
videoStream = canvas.captureStream(targetFps);
var videoTrack = videoStream.getVideoTracks()[0];
if (videoTrack) {
stream.addTrack(videoTrack);
trackCount += 1;
videoTrack.addEventListener(
"ended",
function () {
trackCount -= 1;
if (trackCount <= 0) {
cleanupSyntheticStream();
}
},
{ once: true }
);
}
} else {
warnlog("canvas.captureStream unavailable; synthetic video track disabled.");
}
}
if (wantsAudio) {
if (!getSyntheticAudioContext()) {
warnlog("AudioContext unavailable; synthetic audio track disabled.");
} else {
var requestedTone = parseInt(session.testMediaTone) || 440;
requestedTone = clampSyntheticSetting(requestedTone, 50, 2000, 440);
var destination = audioContext.createMediaStreamDestination();
oscillator = audioContext.createOscillator();
gainNode = audioContext.createGain();
oscillator.type = "sine";
oscillator.frequency.value = requestedTone;
gainNode.gain.value = 0.03;
oscillator.connect(gainNode);
gainNode.connect(destination);
oscillator.start();
if (audioContext.state === "suspended") {
audioContext.resume().catch(function () {});
}
var audioTrack = destination.stream.getAudioTracks()[0];
if (audioTrack) {
stream.addTrack(audioTrack);
trackCount += 1;
audioTrack.addEventListener(
"ended",
function () {
trackCount -= 1;
if (trackCount <= 0) {
cleanupSyntheticStream();
}
},
{ once: true }
);
}
}
}
if (!stream.getTracks().length) {
cleanupSyntheticStream();
var emptyTrackErr = new Error("Unable to create synthetic media tracks.");
emptyTrackErr.name = "NotFoundError";
throw emptyTrackErr;
}
return stream;
}
function enableTestMediaCapture() {
if (syntheticGetUserMediaEnabled) {
return;
}
if (!navigator || !navigator.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== "function") {
return;
}
nativeGetUserMediaForTests = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
navigator.mediaDevices.getUserMedia = function (constraints) {
if (!(session && session.testMedia)) {
return nativeGetUserMediaForTests(constraints);
}
try {
return Promise.resolve(createSyntheticTestMediaStream(constraints || {}));
} catch (err) {
return Promise.reject(err);
}
};
syntheticGetUserMediaEnabled = true;
warnlog("Synthetic test media capture enabled.");
}
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) {
if (e && e.name === "AbortError") {
warnlog("fetchWithTimeout aborted after " + timeout + "ms");
} else {
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, "'");
}
function sanitizeCustomHTML(unsafe = "", maxLength = 4096) {
if (typeof unsafe !== "string") {
return "";
}
let html = unsafe.trim();
if (!html) {
return "";
}
if (maxLength && (html.length > maxLength)) {
html = html.substring(0, maxLength);
}
const allowedTags = new Set([
"a",
"b",
"strong",
"i",
"em",
"u",
"s",
"br",
"p",
"div",
"span",
"ul",
"ol",
"li",
"img",
"hr",
"small",
"sub",
"sup",
"code",
"pre"
]);
const stripTagsCompletely = new Set(["script", "style", "iframe", "object", "embed", "svg", "math", "form", "input", "button", "textarea", "select", "meta", "link"]);
const allowedGlobalAttrs = new Set(["title"]);
const allowedTagAttrs = {
a: new Set(["href", "target", "rel", "title"]),
img: new Set(["src", "alt", "title", "width", "height", "loading"])
};
function sanitizeURL(raw = "", allowImageData = false) {
if (typeof raw !== "string") {
return "";
}
const value = raw.trim();
if (!value) {
return "";
}
if (value.startsWith("#")) {
return value;
}
if (allowImageData) {
// Allow common base64 image embeds, but block svg/data script vectors.
if (/^data:image\/(?:png|jpe?g|gif|webp|avif);base64,[a-z0-9+/=\s]+$/i.test(value)) {
return value;
}
}
try {
const parsed = new URL(value, window.location.href);
const protocol = parsed.protocol.toLowerCase();
if (protocol === "http:" || protocol === "https:" || protocol === "mailto:" || protocol === "tel:") {
return parsed.href;
}
} catch (e) {
return "";
}
return "";
}
function sanitizeTree(root) {
for (const child of Array.from(root.children)) {
sanitizeTree(child);
const tagName = child.tagName.toLowerCase();
if (!allowedTags.has(tagName)) {
if (stripTagsCompletely.has(tagName)) {
child.remove();
continue;
}
const fragment = document.createDocumentFragment();
while (child.firstChild) {
fragment.appendChild(child.firstChild);
}
child.replaceWith(fragment);
continue;
}
for (const attr of Array.from(child.attributes)) {
const name = attr.name.toLowerCase();
const value = attr.value || "";
const allowedForTag = allowedTagAttrs[tagName] || new Set();
const isAllowed = allowedGlobalAttrs.has(name) || allowedForTag.has(name);
if (!isAllowed || name.startsWith("on") || name === "style" || name === "srcdoc") {
child.removeAttribute(attr.name);
continue;
}
if (name === "href") {
const safeHref = sanitizeURL(value, false);
if (!safeHref) {
child.removeAttribute("href");
} else {
child.setAttribute("href", safeHref);
}
continue;
}
if (name === "src") {
const safeSrc = sanitizeURL(value, true);
if (!safeSrc) {
child.removeAttribute("src");
} else {
child.setAttribute("src", safeSrc);
}
continue;
}
if (name === "target") {
const target = value.toLowerCase();
if (!["_blank", "_self"].includes(target)) {
child.setAttribute("target", "_self");
}
}
}
if (tagName === "a") {
const target = (child.getAttribute("target") || "").toLowerCase();
if (target === "_blank") {
child.setAttribute("rel", "noopener noreferrer");
}
}
}
}
const template = document.createElement("template");
template.innerHTML = html;
sanitizeTree(template.content);
return template.innerHTML;
}
function sanitizeRedirectURL(raw = "", maxLength = 2048) {
if (typeof raw !== "string") {
return "";
}
let href = raw.trim();
if (!href) {
return "";
}
if (maxLength && (href.length > maxLength)) {
href = href.substring(0, maxLength);
}
const hasHttpScheme = /^https?:/i.test(href);
const isRelativePath = href.startsWith("/") || href.startsWith("./") || href.startsWith("../") || href.startsWith("?") || href.startsWith("#");
// Keep custom domain redirects working without requiring an explicit https:// scheme,
// but do not rewrite relative page paths like "thanks.html" or "folder/thanks.html".
if (!hasHttpScheme && !isRelativePath && !href.includes(" ") && !href.includes("\\")) {
const hostCandidate = href.split(/[/?#]/, 1)[0] || "";
const hostOnly = hostCandidate.replace(/:\d+$/, "");
const hostParts = hostOnly.split(".");
const tld = (hostParts[hostParts.length - 1] || "").toLowerCase();
const commonTlds = new Set([
"com", "org", "net", "edu", "gov", "mil", "io", "co", "us", "ca", "uk", "gg", "tv", "fm", "me", "app", "dev", "ai",
"biz", "info", "live", "media", "video", "stream", "ninja", "cloud", "online", "site", "tech"
]);
const looksLikeHostname = (hostParts.length >= 2)
&& commonTlds.has(tld)
&& hostParts.every(part => /^[a-z0-9-]+$/i.test(part) && !part.startsWith("-") && !part.endsWith("-"));
const looksLikeIPv4 = /^(\d{1,3}\.){3}\d{1,3}$/.test(hostOnly)
&& hostOnly.split(".").every(octet => Number(octet) >= 0 && Number(octet) <= 255);
const looksLikeLocalhost = (hostOnly.toLowerCase() === "localhost");
if (looksLikeHostname || looksLikeIPv4 || looksLikeLocalhost) {
href = "https://" + href;
}
}
try {
const parsed = new URL(href, window.location.href);
const protocol = (parsed.protocol || "").toLowerCase();
if (protocol === "http:" || protocol === "https:") {
return parsed.href;
}
} catch (e) {}
return "";
}
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;
};
var decodeSanitizedPassword = function (passwrd) {
if (passwrd === "" || passwrd === false || passwrd === null || typeof passwrd === "undefined") {
return "";
}
if (typeof passwrd !== "string") {
passwrd = String(passwrd);
}
try {
return decodeURIComponent(passwrd);
} catch (e) {
return passwrd;
}
};
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 {
if (!window.localStorage || typeof window.localStorage.getItem !== "function") {
return "";
}
} catch (e) {
return "";
}
try {
var itemStr = localStorage.getItem(cname);
} catch (e) {
errorlog(e);
return "";
}
if (!itemStr) {
return "";
}
var item = null;
try {
item = JSON.parse(itemStr);
} catch (e) {
removeStorage(cname);
return "";
}
if (!item || typeof item !== "object") {
removeStorage(cname);
return "";
}
var now = new Date();
if (!("expiry" in item) || 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 escapeJoinRequestText(value) {
return ("" + (value || "")).replace(/[&<>"]/g, function (ch) {
if (ch === "&") return "&";
if (ch === "<") return "<";
if (ch === ">") return ">";
if (ch === '"') return """;
return ch;
});
}
function renderQueueButtonState() {
if (!session || !session.director) {
return;
}
var queueButton = document.getElementById("queuebutton");
var queueBadge = document.getElementById("queueNotification");
if (!queueButton || !queueBadge) {
return;
}
var pendingCount = (session.pendingJoinRequests && session.pendingJoinRequests.length) ? session.pendingJoinRequests.length : 0;
var queueCount = (session.queueList && session.queueList.length) ? session.queueList.length : 0;
var hasPending = pendingCount > 0;
var hasQueue = !!session.queue && queueCount > 0;
var shouldShow = !!session.queue || hasPending || hasQueue;
if (shouldShow) {
queueButton.classList.remove("hidden");
} else {
queueButton.classList.add("hidden");
}
if (hasPending || hasQueue) {
queueButton.classList.add("queueAttention");
} else {
queueButton.classList.remove("queueAttention");
}
var queueTitle = "Load next guest in queue";
if (hasPending && !session.queue) {
queueTitle = "Review pending join requests";
} else if (hasPending && session.queue) {
queueTitle = "Load next guest in queue (pending join approvals waiting)";
}
queueButton.title = queueTitle;
queueButton.setAttribute("aria-label", queueTitle);
var badgeCount = 0;
if (session.queue) {
if (queueCount) {
badgeCount = queueCount;
} else if (pendingCount) {
badgeCount = pendingCount;
}
} else if (pendingCount) {
badgeCount = pendingCount;
}
if (badgeCount) {
queueBadge.innerHTML = badgeCount > 10 ? "‼" : badgeCount;
queueBadge.classList.add("queueNotification");
queueBadge.classList.add("queueNotificationPulse");
} else {
queueBadge.innerHTML = "";
queueBadge.classList.remove("queueNotification");
queueBadge.classList.remove("queueNotificationPulse");
}
}
function updateJoinRequestPanel(adding = false) {
if (!session || !session.director) {
return;
}
session.pendingJoinRequests = session.pendingJoinRequests || [];
session.pendingJoinRequests = session.pendingJoinRequests.filter(request => request && request.UUID);
if (session.pendingJoinPrompted && session.pendingJoinPrompted.size) {
var activeUUIDs = new Set(session.pendingJoinRequests.map(request => request.UUID));
session.pendingJoinPrompted.forEach(function (uuid) {
if (!activeUUIDs.has(uuid)) {
session.pendingJoinPrompted.delete(uuid);
}
});
}
var panel = document.getElementById("joinRequestPanel");
var list = document.getElementById("joinRequestList");
var pendingCount = session.pendingJoinRequests.length;
if (list) {
if (!pendingCount) {
list.innerHTML = '
`;
} 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.
}
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.sessionLog) {
getById("sessionMarkerButton").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");
if (session.sessionLog) {
getById("sessionMarkerButton").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 + getCloudflareInviteParam();
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.sessionLog) {
getById("sessionMarkerButton").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");
if (session.sessionLog) {
getById("sessionMarkerButton").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) {
try {
const parsedExisting = new URL(iframeURL, window.location.href);
const parsedProtocol = (parsedExisting.protocol || "").toLowerCase();
if (parsedProtocol === "http:" || parsedProtocol === "https:") {
return parsedExisting.href;
}
} catch (e) {}
warnlog("Blocked iframe URL with unsupported protocol.");
return "about:blank";
}
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) {
warnlog("Blocked invalid iframe URL.");
return "about:blank";
}
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;
}
}
}
try {
const parsedIframe = new URL(iframeURL, window.location.href);
const iframeProtocol = (parsedIframe.protocol || "").toLowerCase();
if (iframeProtocol !== "http:" && iframeProtocol !== "https:") {
warnlog("Blocked iframe URL with unsupported protocol: " + iframeProtocol);
return "about:blank";
}
return parsedIframe.href;
} catch (e) {
warnlog("Blocked invalid iframe URL.");
return "about:blank";
}
}
function getCloudflareInviteParam() {
if (!session.whipOutput) {
return "";
}
var whipOutput = "";
try {
whipOutput = (session.whipOutput || "").trim();
} catch (e) {
whipOutput = "";
}
if (!whipOutput || !/^https:\/\/cloudflare\.vdo\.ninja\//i.test(whipOutput)) {
return "";
}
var token = session.whipOutputToken || "";
if (!token) {
token = whipOutput.replace(/^https:\/\/cloudflare\.vdo\.ninja\//i, "");
}
if (!token) {
return "";
}
return "&cftoken=" + encodeURIComponent(token);
}
function soloLinkGenerator(streamID, scene = true) {
var codecGroupFlag = "";
if (session.codecGroupFlag) {
codecGroupFlag = session.codecGroupFlag;
}
if (session.bitrateGroupFlag) {
codecGroupFlag += session.bitrateGroupFlag;
}
var wss = "";
if (session.wssSetViaUrl) {
if (session.customWSS && (session.customWSS !== true)) {
wss = "&pie=" + session.customWSS;
} else if (session.customWSS == true) {
wss = "&wss=" + session.wss;
} else {
wss = "&wss2=" + session.wss;
}
}
var passAdd2 = "";
if (session.password) {
if (session.defaultPassword === false) {
passAdd2 = "&password=" + session.password;
}
}
if (session.token) {
passAdd2 += "&token=" + session.token;
}
if (session.claimBypassKey) {
passAdd2 += "&roomkey=" + session.claimBypassKey;
}
// 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";
}
}
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);
}
}
function shouldUseCredentiallessIframe() {
try {
if (typeof HTMLIFrameElement === "undefined" || !("credentialless" in HTMLIFrameElement.prototype)) {
return false;
}
} catch (e) {
return false;
}
return !Firefox;
}
function applyIframeSecurityAttributes(iframe) {
iframe.setAttribute("allowtransparency", "true");
iframe.setAttribute("crossorigin", "anonymous");
if (shouldUseCredentiallessIframe()) {
iframe.setAttribute("credentialless", "true");
} else {
iframe.removeAttribute("credentialless");
}
}
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";
applyIframeSecurityAttributes(iframe);
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);
postLoudnessToIframe(loudnessObj, total);
}
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;
try {
sessionStorage.removeItem('vdo_pending_room_settings');
sessionStorage.removeItem('vdo_pending_room_settings_recover');
} catch (e2) {}
}
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 + getCloudflareInviteParam() + "&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 + getCloudflareInviteParam() + "&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 + getCloudflareInviteParam() + 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) {
var passwordRoom = "";
var createdViaWizard = (reload !== true) && (roomname === false);
if (reload === true) {
let oldDirectorSettings = getStorage("directorOtherSettings");
if (!oldDirectorSettings || typeof oldDirectorSettings !== "object") {
warnUser("Couldn't load previous session");
return;
}
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.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);
if (createdViaWizard) {
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;
updateURL("auth");
// 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 };
// Persist only when an OAuth redirect is expected.
if (!session.authToken && !session.universalToken) {
try {
sessionStorage.setItem('vdo_pending_room_settings', JSON.stringify(session.pendingRoomSettings));
sessionStorage.setItem('vdo_pending_room_settings_recover', '1');
} catch(e2){}
} else {
try {
sessionStorage.removeItem('vdo_pending_room_settings');
sessionStorage.removeItem('vdo_pending_room_settings_recover');
} catch(e2){}
}
// If guests must sign in (authenticated/allowlist), mark as requireAuth for UX
if (accessMode === 'authenticated' || accessMode === 'allowlist') {
session.requireAuth = true;
}
}
} catch (e) { errorlog(e); }
}
if (session.authMode && !session.pendingRoomSettings) {
// Recover settings after OAuth redirect (checkbox state lost on reload)
try {
var shouldRecover = sessionStorage.getItem('vdo_pending_room_settings_recover') === '1';
var stored = sessionStorage.getItem('vdo_pending_room_settings');
if (shouldRecover && stored) {
session.pendingRoomSettings = JSON.parse(stored);
if (session.pendingRoomSettings.accessMode === 'authenticated' || session.pendingRoomSettings.accessMode === 'allowlist') {
session.requireAuth = true;
}
}
sessionStorage.removeItem('vdo_pending_room_settings');
sessionStorage.removeItem('vdo_pending_room_settings_recover');
} catch(e2){}
}
}
var parsedClaimCap = parseInt(session.claimRoomCap);
if (Number.isFinite(parsedClaimCap) && parsedClaimCap > 0) {
session.claimRoomCap = parsedClaimCap;
} else {
session.claimRoomCap = false;
}
session.claimBypassKey = sanitizePassword(session.claimBypassKey || "") || false;
session.roomBypassKey = sanitizePassword(session.roomBypassKey || "") || false;
if (!session.claimBypassKey && session.roomBypassKey) {
session.claimBypassKey = session.roomBypassKey;
} else if (!session.roomBypassKey && session.claimBypassKey) {
session.roomBypassKey = session.claimBypassKey;
}
session.requireServerApproval = session.requireServerApproval === true;
try {
if (createdViaWizard) {
var roomApprovalToggle = getById("requireApprovalForRoom");
if (roomApprovalToggle) {
session.requireServerApproval = !!roomApprovalToggle.checked;
}
}
if (createdViaWizard && session.requireServerApproval && !session.claimBypassKey) {
var generatedRoomKey = "";
if (session.generateStreamID && typeof session.generateStreamID === "function") {
generatedRoomKey = session.generateStreamID(14);
} else {
generatedRoomKey = Math.random().toString(36).substring(2, 16);
}
generatedRoomKey = sanitizePassword(generatedRoomKey || "");
if (generatedRoomKey) {
session.claimBypassKey = generatedRoomKey;
session.roomBypassKey = generatedRoomKey;
updateURL("roomkey=" + generatedRoomKey, true, false);
}
}
if (session.requireServerApproval) {
updateURL("requireapproval");
} else if (urlParams.has("requireapproval")) {
var href = new URL(window.location.href);
href.searchParams.delete("requireapproval");
if (!session.nohistory) {
window.history.pushState({ path: href.toString() }, "", href.toString());
}
urlParams = mergeFragmentParams(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);
}
}
} catch (e) {
errorlog(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 buildCoDirectorInviteURL() {
var token = "";
if (session.token) {
token += "&token=" + session.token;
}
var roomKeyParam = session.claimBypassKey ? "&roomkey=" + session.claimBypassKey : "";
var url = "https://" + location.host + location.pathname + "?dir=" + session.roomid + getCloudflareInviteParam() + "&codirector=" + session.directorPassword + token + roomKeyParam;
if (session.codirectorNoClaim) {
url += "&noclaim";
}
if (session.approval_popup) {
url += "&approvepopup";
}
var implicitAuthSecret = session.authMode && session.authImplicitRoomSecret && (session.password === session.authImplicitRoomSecret);
if (implicitAuthSecret) {
url += "&auth";
}
if ((session.password !== session.sitePassword) && !implicitAuthSecret) {
if (session.password === false) {
url += "&password=false";
} else {
url += "&password=" + session.password;
}
}
return url;
}
function updateCoDirectorInviteLink() {
if (getById("codirectorSettings_invite")) {
getById("codirectorSettings_invite").value = buildCoDirectorInviteURL();
}
}
function toggleCoDirector_noClaim(ele) {
session.codirectorNoClaim = ele.checked;
updateCoDirectorInviteLink();
}
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 {
updateCoDirectorInviteLink();
} 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;
}
if (getById("codirectorSettings_noclaim")) {
getById("codirectorSettings_noclaim").checked = !!session.codirectorNoClaim;
}
updateCoDirectorInviteLink();
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;
try {
if (sessionStorage.getItem('vdo_sso_disabled_notice') === '1') {
sessionStorage.removeItem('vdo_sso_disabled_notice');
warnUser("SSO has been disabled for this director room. New guest links will not include SSO, and previous SSO guest invites may not work with this room.", 6000);
}
} catch (e) {}
session.pendingJoinRequests = [];
session.pendingJoinPrompted = new Set([]);
updateJoinRequestPanel(false);
toggleJoinRequestPanel(false);
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";
updateCoDirectorInviteLink();
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;
}
if (getById("codirectorSettings_noclaim")) {
getById("codirectorSettings_noclaim").checked = !!session.codirectorNoClaim;
}
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;
}
var roomKeyParam = "";
if (session.claimBypassKey) {
roomKeyParam = "&roomkey=" + session.claimBypassKey;
}
// Add auth parameters if in auth mode
var authParams = "";
if (session.authMode) {
authParams = "&auth";
// 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 + roomKeyParam;
getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams + roomKeyParam;
getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams + roomKeyParam;
// Update all solo links
updateAllSoloLinks();
}
});
}
}
var cloudflareInviteParam = getCloudflareInviteParam();
getById("director_block_1").dataset.raw = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams + cloudflareInviteParam;
getById("director_block_1").href = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams + cloudflareInviteParam;
getById("director_block_1").innerText = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams + cloudflareInviteParam;
// 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 + roomKeyParam;
getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams + roomKeyParam;
getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams + roomKeyParam;
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 = { 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));
label.title = "Hold Ctrl to select multiple";
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);
}
// Add ASIO devices if available (Windows only via Electron Capture)
// Try sync first, then async for sandbox mode
addAsioDevicesToDropdown(audioInputSelect, counter);
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) {
let constraints = {};
switch (resolutionFallbackLevel) {
case -1:
constraints = {};
break;
case -2:
if (isSafariBrowser) {
constraints = {
width: {
min: 360,
ideal: 3840,
max: 3840
},
height: {
min: 360,
ideal: 2160,
max: 2160
}
};
} else if (Firefox) {
constraints = {
width: {
ideal: 3840
},
height: {
ideal: 2160
}
};
} else {
constraints = {
width: {
min: 720,
ideal: 3840,
max: 3840
},
height: {
min: 720,
ideal: 2160,
max: 2160
}
};
}
break;
case -3:
if (isSafariBrowser) {
constraints = {
width: {
min: 360,
ideal: 2560,
max: 1440
},
height: {
min: 360,
ideal: 1440,
max: 1440
}
};
} else if (Firefox) {
constraints = {
width: {
ideal: 2560
},
height: {
ideal: 1440
}
};
} else {
constraints = {
width: {
min: 720,
ideal: 2560,
max: 2560
},
height: {
min: 720,
ideal: 1440,
max: 1440
}
};
}
break;
case 0:
if (isSafariBrowser) {
constraints = {
width: {
min: 360,
ideal: 1920,
max: 1920
},
height: {
min: 360,
ideal: 1080,
max: 1080
}
};
} else if (Firefox) {
constraints = {
width: {
ideal: 1920
},
height: {
ideal: 1080
}
};
} else {
constraints = {
width: {
min: 720,
ideal: 1920,
max: 1920
},
height: {
min: 720,
ideal: 1080,
max: 1920
}
};
}
break;
case 1:
if (isSafariBrowser) {
constraints = {
width: {
min: 360,
ideal: 1280,
max: 1280
},
height: {
min: 360,
ideal: 720,
max: 720
}
};
} else if (Firefox) {
constraints = {
width: {
ideal: 1280
},
height: {
ideal: 720
}
};
} else {
constraints = {
width: {
min: 720,
ideal: 1280,
max: 1280
},
height: {
min: 720,
ideal: 720,
max: 1280
}
};
}
break;
case 2:
if (isSafariBrowser) {
constraints = {
width: {
min: 640
},
height: {
min: 360
}
};
} else if (Firefox) {
constraints = {
width: {
ideal: 640
},
height: {
ideal: 360
}
};
} else {
constraints = {
width: {
min: 240,
ideal: 640,
max: 1280
},
height: {
min: 240,
ideal: 360,
max: 1280
}
};
}
break;
case 3:
constraints = {
width: {
min: 360,
ideal: 1280,
max: 1440
}
};
break;
case 4:
if (isSafariBrowser) {
constraints = {
height: {
min: 360,
ideal: 720,
max: 960
}
};
} else {
constraints = {
height: {
ideal: 720,
max: 960
}
};
}
break;
case 5:
if (isSafariBrowser) {
constraints = {
width: {
min: 360,
ideal: 640,
max: 1440
},
height: {
min: 360,
ideal: 360,
max: 720
}
};
} else {
constraints = {
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
}
break;
case 6:
if (isSafariBrowser) {
constraints = {}; // iphone users probably don't need to wait any longer, so let them just get to it
} else {
constraints = {
width: {
min: 360,
ideal: 640,
max: 3840
},
height: {
min: 360,
ideal: 360,
max: 2160
}
};
}
break;
case 7:
constraints = {
// 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
}
};
break;
case 8:
constraints = {
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
break;
case 9:
constraints = {
frameRate: 0
}; // Some Samsung Devices report they can only support a frameRate of 0.
break;
case 10:
constraints = {};
break;
default:
constraints = {};
break;
}
return constraints;
}
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));
label.title = "Hold Ctrl to select multiple";
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);
}
// Add ASIO devices if available (Windows only via Electron Capture)
// Try sync first, then async for sandbox mode
addAsioDevicesToDropdown(audioInputSelect, counter);
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);
}
function restartWhipDirector(ele) {
var UUID = ele.dataset.UUID;
if (UUID && session.rpcs[UUID]) {
meshRestartWhip(UUID);
warnUser("WHIP restart command sent");
}
}
// ============================================
// 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 (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) {
if (session.buffer !== false) {
buffer = session.buffer || 0;
} else if (session.chunkbuffer !== false) {
buffer = session.chunkbuffer || 0;
} else {
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);
session.totalRoomBitrate_userSet = true;
var msg = {};
msg.directorSettings = {};
msg.directorSettings.totalRoomBitrate = session.totalRoomBitrate;
session.sendMessage(msg);
pokeIframeAPI("total-room-bitrate", session.totalRoomBitrate);
}
function sendMediaDevices(UUID) {
enumerateDevices().then(async function (deviceInfos) {
// Add ASIO devices if available (Windows only via Electron Capture)
try {
var asioAvailable = await electronSupportsAsioAsync();
if (asioAvailable) {
var asioDevices = await getAsioDevicesAsync();
if (asioDevices && asioDevices.length > 0) {
asioDevices.forEach(function(device) {
deviceInfos.push({
deviceId: "asio:" + device.index,
kind: "audioinput",
label: "ASIO: " + device.name + " (" + device.maxInputChannels + "ch)",
groupId: "asio"
});
});
}
}
} catch (e) {
// ASIO not available, continue with standard devices
}
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");
var code = "";
var type = "unknown";
try {
if (event && event.type) {
type = event.type;
}
if (event && event.currentTarget && event.currentTarget.error && ("code" in event.currentTarget.error)) {
code = event.currentTarget.error.code;
}
} catch (e) { }
errorlog("video error detail: type=" + type + (code ? " code=" + code : ""));
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.
const selectedLabel = videoSelect.options[videoSelect.selectedIndex] ? videoSelect.options[videoSelect.selectedIndex].text : "";
const isObsCam = selectedLabel.startsWith("OBS-Camera") || selectedLabel.startsWith("OBS Virtual Camera") || selectedLabel.startsWith("Streamlabs ");
if (isObsCam && !session.frameRate && session.maxframeRate == false) {
// Firefox + OBS Virtual Camera can fail or stick on device switches unless a 30fps cap is applied.
// Scope the cap to OBS only so other cameras (eg. Cam Link) retain their native fps/resolution behavior.
constraints.video.frameRate = {
ideal: 30,
max: 30
};
}
} 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("Camera error: " + (e && e.name ? e.name : "unknown") + " " + (e && e.message ? e.message : ""));
}
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: " + (e && e.name ? e.name : "unknown") + " " + (e && e.message ? e.message : ""));
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, preserveSelectedAudio = 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, preserveSelectedAudio); // 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 handleAudioReplaceFailure(err, UUID, track, videoSource = null, context = "senderAudioUpdate") {
try {
errorlog(err);
errorlog("replaceTrack(audio) failed");
if (session.bumpReliabilityCounter) {
session.bumpReliabilityCounter("audio_replace_failures");
}
warnlog("Audio sender fallback repair attempted for " + UUID + " (" + context + ")");
attemptPeerAudioRepair(UUID, track, videoSource, context);
} catch (e) {
errorlog(e);
}
}
function replaceAudioTrackSafely(sender, track, UUID, videoSource = null, context = "senderAudioUpdate") {
try {
if (!sender || typeof sender.replaceTrack !== "function") {
warnlog("replaceAudioTrackSafely called with invalid sender");
return Promise.resolve(false);
}
var result = sender.replaceTrack(track);
if (result && typeof result.then === "function") {
return result
.then(function () {
return true;
})
.catch(function (err) {
handleAudioReplaceFailure(err, UUID, track, videoSource, context);
return false;
});
}
return Promise.resolve(true);
} catch (err) {
handleAudioReplaceFailure(err, UUID, track, videoSource, context);
return Promise.resolve(false);
}
}
function enableSenderAfterAudioReplace(sender, track, replaceResult) {
try {
if (track) {
track.enabled = true;
}
return Promise.resolve(replaceResult)
.then(function (replaced) {
if (!replaced) {
return false;
}
if (sender && sender.track) {
sender.track.enabled = true;
}
return true;
})
.catch(function (e) {
errorlog(e);
return false;
});
} catch (e) {
errorlog(e);
return Promise.resolve(false);
}
}
async function attemptPeerAudioRepair(UUID, track, videoSource = null, context = "senderAudioUpdate") {
try {
if (!UUID || !track) {
return false;
}
if (!session.audioRepairInFlight) {
session.audioRepairInFlight = {};
}
if (session.audioRepairInFlight[UUID]) {
warnlog("Audio repair already in-flight for " + UUID + "; skipping duplicate request");
return false;
}
session.audioRepairInFlight[UUID] = Date.now();
if (session.bumpReliabilityCounter) {
session.bumpReliabilityCounter("audio_repair_attempts");
}
var repaired = false;
var pc = session.pcs && session.pcs[UUID] ? session.pcs[UUID] : null;
var settleDelayMs = parseInt(session.audioRepairSettleMs) || 1200;
if (!pc) {
warnlog("Audio repair skipped: no peer connection for " + UUID);
if (session.bumpReliabilityCounter) {
session.bumpReliabilityCounter("audio_repair_failures");
}
return false;
}
try {
var senders = getSenders2(UUID);
var audioSender = senders.find(s => s.track && s.track.kind === "audio");
if (audioSender && typeof audioSender.replaceTrack === "function") {
try {
await Promise.resolve(audioSender.replaceTrack(track));
if (audioSender.track) {
audioSender.track.enabled = true;
}
repaired = true;
} catch (e) {
errorlog(e);
}
}
if (!repaired) {
// Try to recover null-track audio transceivers before falling back to addTrack.
var allSenders = pc.getSenders ? pc.getSenders() : [];
var hasAudioSender = allSenders.some(function(s) {
return s.track && s.track.kind === "audio";
});
var nullAudioSender = null;
if (pc.getTransceivers) {
var transceivers = pc.getTransceivers();
nullAudioSender = transceivers.find(function (t) {
return t &&
t.sender &&
typeof t.sender.replaceTrack === "function" &&
!t.sender.track &&
t.receiver &&
t.receiver.track &&
t.receiver.track.kind === "audio";
});
}
if (!hasAudioSender && nullAudioSender) {
try {
await Promise.resolve(nullAudioSender.sender.replaceTrack(track));
if (nullAudioSender.sender.track) {
nullAudioSender.sender.track.enabled = true;
}
repaired = true;
} catch (e) {
errorlog(e);
}
}
if (!repaired && hasAudioSender) {
warnlog("Audio sender exists but replaceTrack failed; skipping addTrack to avoid duplicate for " + UUID);
} else if (!repaired) {
let source = videoSource;
if (!source || typeof source.getTracks !== "function") {
if (session.videoElement && session.videoElement.srcObject &&
typeof session.videoElement.srcObject.getTracks === "function") {
source = session.videoElement.srcObject;
} else if (session.streamSrc && typeof session.streamSrc.getTracks === "function") {
source = session.streamSrc;
}
}
if (source) {
try {
pc.addTrack(track, source);
repaired = true;
} catch (e) {
errorlog(e);
}
}
}
}
if (repaired && typeof session.createOffer === "function" && pc.signalingState === "stable") {
try {
session.createOffer(UUID, true);
} catch (e) {
warnlog(e);
}
}
} catch (e) {
errorlog(e);
}
if (repaired) {
if (settleDelayMs > 0) {
// Keep in-flight state around briefly while renegotiation starts.
await new Promise(function (resolve) {
setTimeout(resolve, settleDelayMs);
});
}
warnlog("Audio sender fallback repair succeeded for " + UUID + " (" + context + ")");
return true;
}
if (session.bumpReliabilityCounter) {
session.bumpReliabilityCounter("audio_repair_failures");
}
errorlog("Audio sender fallback repair failed for " + UUID + " (" + context + ")");
return false;
} catch (e) {
errorlog(e);
if (session.bumpReliabilityCounter) {
session.bumpReliabilityCounter("audio_repair_failures");
}
return false;
} finally {
if (session.audioRepairInFlight) {
delete session.audioRepairInFlight[UUID];
}
}
}
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 => {
try {
var replaceResult = sender.replaceTrack(trk);
if (replaceResult && typeof replaceResult.then === "function") {
replaceResult.catch(function (err) {
errorlog(err);
errorlog("replaceTrack(audio) failed");
if (session.bumpReliabilityCounter) {
session.bumpReliabilityCounter("audio_replace_failures");
}
warnlog("WHIP audio replaceTrack failed; no peer-level fallback available");
});
}
} catch (e) {
errorlog(e);
errorlog("replaceTrack(audio) failed");
if (session.bumpReliabilityCounter) {
session.bumpReliabilityCounter("audio_replace_failures");
}
warnlog("WHIP audio replaceTrack threw synchronously; no peer-level fallback available");
}
});
}
});
}
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") {
var replaceResult = replaceAudioTrackSafely(sender, trk, UUID, STRM, "senderAudioUpdate:mixMinus");
enableSenderAfterAudioReplace(sender, trk, replaceResult);
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) {
var replaceResult = replaceAudioTrackSafely(sender, track, UUID, videoSource, "senderAudioUpdate:reuse-disabled-sender");
enableSenderAfterAudioReplace(sender, track, replaceResult);
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();
// Update director mix-minus for all guests when director's mic changes
if (session.directorMixMinus && session.mixMinusState) {
for (var mmUUID in session.mixMinusState) {
if (session.mixMinusState[mmUUID].enabled && session.pcs[mmUUID]) {
try {
updateMixMinusForGuest(mmUUID);
} catch (e) {
errorlog(e);
}
}
}
}
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.sessionLog) {
getById("sessionMarkerButton").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 = sanitizeCustomHTML(session.welcomeHTML, 8192);
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 = [];
var drawingRequestList = [];
function notifyChatActionAvailable() {
if (session.chatbutton === false) {
return;
}
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");
}
}
function addDrawingPermissionRequest(UUID, altUUID = false, targetUUID = false, targetIsScreen = false) {
try {
var requesterUUID = UUID;
if ((!requesterUUID || !session.pcs || !session.pcs[requesterUUID]) && altUUID && session.pcs && session.pcs[altUUID]) {
requesterUUID = altUUID;
}
if (session.scene !== false || session.view || session.cleanOutput) {
if (session.resolveDrawingRequest) {
session.resolveDrawingRequest(requesterUUID, false, altUUID, targetUUID, targetIsScreen);
}
return;
}
if (session.chatbutton === false) {
if (session.resolveDrawingRequest) {
session.resolveDrawingRequest(requesterUUID, false, altUUID, targetUUID, targetIsScreen);
}
return;
}
if (!requesterUUID || !session.pcs || !session.pcs[requesterUUID]) {
return;
}
if (session.pcs[requesterUUID].drawControlAllowed) {
if (session.resolveDrawingRequest) {
session.resolveDrawingRequest(requesterUUID, true, altUUID, targetUUID, targetIsScreen);
}
return;
}
for (var i = 0; i < drawingRequestList.length; i++) {
if (drawingRequestList[i].UUID === requesterUUID && drawingRequestList[i].targetUUID === targetUUID && drawingRequestList[i].status === 0) {
return;
}
}
drawingRequestList.push({
UUID: requesterUUID,
altUUID,
targetUUID,
targetIsScreen: !!targetIsScreen,
status: 0,
time: Date.now()
});
notifyChatActionAvailable();
} catch (e) {
errorlog(e);
}
}
function resolveDrawingPermissionRequest(ele, approved) {
try {
const idx = parseInt(ele.dataset.drawingIdx);
const request = drawingRequestList[idx];
if (!request) {
return;
}
request.status = approved ? 1 : 2;
if (session.resolveDrawingRequest) {
session.resolveDrawingRequest(request.UUID, approved, request.altUUID, request.targetUUID, request.targetIsScreen);
}
updateMessages();
} catch (e) {
errorlog(e);
}
}
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() {
msgTransferList = [];
if (session.nodownloads) {
return;
} // downloads are blocked
if (!transferList || !transferList.length) {
return;
}
for (var i = 0; i < transferList.length; i++) {
fileShareMessage(transferList[i], i);
}
}
function showDrawingPermissionRequests() {
if (!drawingRequestList || !drawingRequestList.length) {
return;
}
for (var i = 0; i < drawingRequestList.length; i++) {
drawingPermissionRequestMessage(drawingRequestList[i], i);
}
}
function drawingPermissionRequestMessage(request, idx) {
if (!request || request.status === 3) {
return;
}
var peer = session.pcs && session.pcs[request.UUID] ? session.pcs[request.UUID] : false;
if ((!peer || !peer.label) && request.altUUID && session.pcs && session.pcs[request.altUUID]) {
peer = session.pcs[request.altUUID];
}
var label = peer && peer.label ? sanitizeLabel(peer.label) : ("Guest " + request.UUID.substring(0, 8));
var targetUUID = request.altUUID || request.UUID;
var safeTarget = escapeHtml(targetUUID);
var data = {};
data.idx = "drawing_" + idx;
if (request.status === 0) {
data.msg = " wants to draw or ping on your video. Allow annotation? ";
} else if (request.status === 1) {
data.msg = " drawing access allowed.";
} else if (request.status === 2) {
data.msg = " drawing access denied.";
} else {
return;
}
data.label = "" + label + "";
data.type = "action";
data.time = request.time;
msgTransferList.push(data);
}
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");
applyIframeSecurityAttributes(iframe);
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");
applyIframeSecurityAttributes(iframe);
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) {
// Clean up previous nodes for this uid to prevent resource leaks
if (!session.p2pMixMinusNodes) {
session.p2pMixMinusNodes = {};
}
if (uid && session.p2pMixMinusNodes[uid]) {
try {
var oldNodes = session.p2pMixMinusNodes[uid];
for (var n = 0; n < oldNodes.length; n++) {
try { oldNodes[n].disconnect(); } catch (e) {}
}
} catch (e) {}
delete session.p2pMixMinusNodes[uid];
}
var trackedNodes = [];
if (session.stereo === false) {
var merger = session.audioCtx.createChannelMerger(1);
} else {
var merger = session.audioCtx.createChannelMerger(2);
}
trackedNodes.push(merger);
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);
trackedNodes.push(trackStream);
if (session.stereo !== false) {
var splitter = session.audioCtx.createChannelSplitter(2);
trackedNodes.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); // 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);
trackedNodes.push(trackStream);
if (session.stereo !== false) {
var splitter = session.audioCtx.createChannelSplitter(2);
trackedNodes.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); // hack.
} catch (e) {
errorlog(e);
}
}
} else {
trackStream.connect(merger, 0, 0);
}
} catch (e) {
errorlog(e);
}
}
}
var destination = session.audioCtx.createMediaStreamDestination();
trackedNodes.push(destination);
merger.connect(destination);
if (uid) {
session.p2pMixMinusNodes[uid] = trackedNodes;
}
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") {
replaceAudioTrackSafely(senders[i], mixTracks[0], uuid, mixStream, "updateMixMinusForGuest");
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);
}
var speechError = "unknown";
try {
if (event && event.error) {
speechError = event.error;
} else if (event && event.type) {
speechError = event.type;
}
} catch (e) { }
errorlog("SpeechRecognition error: " + speechError);
};
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 {
if (session.sessionLog && session.sessionLogTranscript) {
pushSessionLogEntry("transcript", session.label || "Me", Final_transcript);
}
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 = buildRecordingFilenameBase(session.rpcs[UUID].label, session.rpcs[UUID].streamID, UUID, 55);
} else {
filename = buildRecordingFilenameBase(UUID, false, "recording", 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 rpc = session.rpcs[UUID] || {};
const filename = buildRecordingFilenameBase(rpc.label, rpc.streamID, UUID || "recording", 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, true);
});
}
// 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 (" + i + "): " + (e && e.message ? e.message : e));
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: " + (e && e.message ? e.message : 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: " + (e && e.message ? e.message : e)));
}
// 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: " + (e && e.message ? e.message : e)));
}
// 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 => {
var workerError = "unknown";
try {
if (event && event.message) {
workerError = event.message;
} else if (event && event.type) {
workerError = event.type;
}
} catch (e) { }
errorlog("focus_worker error: " + workerError);
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 ? 0 : 1;
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(resolveMediaContextElement(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") {
var drawingElement = taskItemInContext;
if (drawingElement.clearDrawOnVideo) {
drawingElement.clearDrawOnVideo();
drawingElement.clearDrawOnVideo = null;
taskItemInContext.clearDrawOnVideo = null;
} else {
var clearDraw = drawOnThis(drawingElement);
if (clearDraw) {
drawingElement.clearDrawOnVideo = clearDraw;
taskItemInContext.clearDrawOnVideo = clearDraw;
}
}
} 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") {
var mediaElement = resolveMediaContextElement(taskItemInContext);
//getById("main").classList.add("forcecontrols"); // adds an annoying shadow to the bar area
//taskItemInContext.showControlBar = true;
//checkVideoControlBar(taskItemInContext);
//taskItemInContext.controls = false;
//ele.focus();
mediaElement.removeAttribute("controls");
mediaElement.setAttribute("controls", "");
mediaElement.controls = true;
taskItemInContext.controls = mediaElement.controls;
} else if (link.getAttribute("data-action") === "HideControls") {
var mediaElement = resolveMediaContextElement(taskItemInContext);
//taskItemInContext.showControlBar = false;
mediaElement.controls = false;
mediaElement.removeAttribute("controls");
taskItemInContext.controls = mediaElement.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") {
// Remote Reload Page - basic director privilege, no &remote required
if (session.rpcs[taskItemInContext.dataset.UUID]) {
var confirmReload = confirm(getTranslation("confirm-reload-user"));
if (confirmReload) {
var msg = {};
msg.reload = true;
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") === "DrawOnVideo") {
var drawLabel = items[i].querySelector("[data-translate='draw-on-video']") || items[i].querySelector("span");
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "screensharesource" || taskItemInContext.id == "previewWebcam") {
items[i].parentNode.classList.remove("hidden");
if (drawLabel) {
drawLabel.textContent = taskItemInContext.clearDrawOnVideo ? "Stop draw mode" : "Draw/Ping on video";
}
} else if (taskItemInContext.dataset && taskItemInContext.dataset.UUID && session.rpcs && session.rpcs[taskItemInContext.dataset.UUID]) {
items[i].parentNode.classList.remove("hidden");
if (drawLabel) {
drawLabel.textContent = taskItemInContext.clearDrawOnVideo
? "Stop draw mode"
: (session.rpcs[taskItemInContext.dataset.UUID].allowDrawing ? "Draw/Ping on video" : "Request draw access");
}
} 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");
}
} else if (items[i].getAttribute("data-action") === "RemoteReload") {
// Remote Reload Page - show for any valid RPC connection (basic director privilege)
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");
}
}
}
}
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);
if (session.sessionLog && !msg.startsWith("/")) {
pushSessionLogEntry("chat", session.label || "Me", 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;
}
if (session.sessionLog) {
var chatSource = label ? sanitizeLabel(label) : (director ? "Director" : "Someone");
pushSessionLogEntry("chat", chatSource, msg);
}
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.sessionLog && session.sessionLogTranscript && msg.isFinal) {
pushSessionLogEntry("transcript", label || "Unknown", transcript);
}
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();
showDrawingPermissionRequests();
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 === "tokens" || data.type === "error") {
if (!dropboxAuthFlowPromise) {
warnlog("Ignored Dropbox auth message without active auth flow");
return;
}
if (!dropboxAuthWindow || event.source !== dropboxAuthWindow) {
warnlog("Ignored Dropbox auth message from unexpected window 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.audioOnly && configureRecording.bitrate < 0) {
configureRecording.bitrate = Math.abs(configureRecording.bitrate);
}
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 = buildRecordingFilenameBase(session.rpcs[UUID].label, session.rpcs[UUID].streamID);
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) {
var recError = "unknown";
try {
if (event && event.error && event.error.name) {
recError = event.error.name + (event.error.message ? (": " + event.error.message) : "");
} else if (event && event.type) {
recError = event.type;
}
} catch (e) { }
errorlog("MediaRecorder error: " + recError);
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";
}
}
if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") {
try {
window.dispatchEvent(
new CustomEvent("vdoninja:remote-recorder-status", {
detail: {
UUID: UUID,
recorder: recorder,
screen: screen
}
})
);
} catch (e) { }
}
}
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 = "";
}
}
}
var sessionLogData = [];
var sessionLogStartTime = 0;
var sessionLogDownloaded = false;
function pushSessionLogEntry(type, source, content) {
if (!session.sessionLog) { return; }
if (!sessionLogStartTime) {
sessionLogStartTime = Date.now();
}
var timeMs = Date.now() - sessionLogStartTime;
sessionLogData.push({
time: timeMs / 1000,
type: type,
source: source || "",
content: content || ""
});
}
function formatSessionLogTimecode(seconds) {
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = seconds % 60;
var ms = Math.round((s - Math.floor(s)) * 1000);
s = Math.floor(s);
if (h > 0) {
return h + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0") + "." + String(ms).padStart(3, "0");
}
return m + ":" + String(s).padStart(2, "0") + "." + String(ms).padStart(3, "0");
}
async function dropSessionMarker() {
var markerCount = 0;
for (var i = 0; i < sessionLogData.length; i++) {
if (sessionLogData[i].type === "marker") {
markerCount += 1;
}
}
var markerNum = markerCount + 1;
var defaultLabel = "Marker #" + markerNum;
// capture timestamp before the prompt
if (!sessionLogStartTime) {
sessionLogStartTime = Date.now();
}
var timeMs = Date.now() - sessionLogStartTime;
var note = await promptAlt("Session Marker #" + markerNum, false, false, false, false, false, false, {placeholder: "Type your note here..."});
var label = (note && note.trim()) ? note.trim() : defaultLabel;
sessionLogData.push({
time: timeMs / 1000,
type: "marker",
source: "",
content: label
});
log("Session marker dropped: " + label);
var btn = getById("sessionMarkerButton");
if (btn) {
btn.style.background = "rgba(255,100,100,0.6)";
btn.title = label + " dropped";
setTimeout(function () {
btn.style.background = "";
}, 500);
}
}
function downloadSessionLog() {
if (!session.sessionLog) { return; }
if (!sessionLogData.length) { return; }
if (sessionLogDownloaded) { return; }
sessionLogDownloaded = true;
var csv = "index,time_seconds,timecode,type,source,content\n";
for (var i = 0; i < sessionLogData.length; i++) {
var entry = sessionLogData[i];
var timecode = formatSessionLogTimecode(entry.time);
var content = (entry.content || "").replace(/"/g, '""');
var source = (entry.source || "").replace(/"/g, '""');
csv += (i + 1) + "," + entry.time.toFixed(3) + ',"' + timecode + '",' + entry.type + ',"' + source + '","' + content + '"\n';
}
var timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
var filename = "vdo-ninja-session-log-" + (session.roomid || session.streamID || "local") + "-" + timestamp + ".csv";
try {
var blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
} catch (e) {
errorlog(e);
}
}
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 = '';
pushSessionLogEntry("recording", "", "Recording started");
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) {
pushSessionLogEntry("recording", "", "Recording stopped");
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
function normalizeRecordingFilenamePart(value) {
if (value === false || value === true || value === null || typeof value === "undefined") {
return "";
}
value = value.toString().trim();
if (!value || value === "false" || value === "null" || value === "undefined") {
return "";
}
return value;
}
function buildRecordingFilenameBase(primary, secondary, fallback = "recording", maxLength = 200) {
var filename = normalizeRecordingFilenamePart(primary) || normalizeRecordingFilenamePart(secondary) || normalizeRecordingFilenamePart(fallback) || "recording";
maxLength = parseInt(maxLength) || 200;
filename = filename.replace(/[\W]+/g, "_").replace(/^_+|_+$/g, "");
filename = filename.substring(0, maxLength);
return filename || "recording";
}
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") {
if (session.gdrive && session.gdrive.sessionUri) {
log("Restarting recording to attach Google Drive upload");
video.recorder.stop();
setTimeout(function() {
recordLocalVideo("start", configureRecording, remote, altUUID);
}, 1000);
return;
}
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 = buildRecordingFilenameBase(session.label, session.streamID);
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) {
var recError = "unknown";
try {
if (event && event.error && event.error.name) {
recError = event.error.name + (event.error.message ? (": " + event.error.message) : "");
} else if (event && event.type) {
recError = event.type;
}
} catch (e) { }
errorlog("MediaRecorder error: " + recError);
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) {
var trackInfo = "";
try {
if (e1 && e1.type) {
trackInfo += " type=" + e1.type;
}
if (e1 && e1.track) {
if (e1.track.kind) {
trackInfo += " kind=" + e1.track.kind;
}
}
} catch (e) { }
errorlog("Remote stream track error" + trackInfo);
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 shouldApplyIncomingViewChroma(UUID) {
if (!session || !session.viewChroma || session.director || !session.rpcs || !session.rpcs[UUID]) {
return false;
}
var rpc = session.rpcs[UUID];
if (!rpc.videoElement || !rpc.streamSrc || rpc.videoMuted || rpc.virtualHangup || rpc.bandwidthMuted || rpc.directorVideoMuted) {
return false;
}
try {
return !!rpc.streamSrc.getVideoTracks().length;
} catch (e) {
return false;
}
}
function createIncomingViewChromaCanvas(UUID) {
if (!session.rpcs[UUID]) {
return false;
}
var rpc = session.rpcs[UUID];
if (rpc.viewChromaCanvas && rpc.viewChromaCanvasCtx) {
return rpc.viewChromaCanvas;
}
var canvas = document.createElement("canvas");
canvas.id = "viewchroma_" + UUID;
canvas.className = "tile";
canvas.style.pointerEvents = "auto";
canvas.style.backgroundColor = "transparent";
canvas.dataset.UUID = UUID;
if (rpc.streamID) {
canvas.dataset.sid = rpc.streamID;
}
canvas.dataset.menu = "context-menu-video";
if (!session.cleanOutput) {
canvas.classList.add("task");
}
canvas.addEventListener("click", function (e) {
try {
var uid = e.currentTarget.dataset.UUID;
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
if (session.statsMenu !== false && session.rpcs[uid] && "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 (session.rpcs[uid] && "prePausedBandwidth" in session.rpcs[uid]) {
unPauseVideo(session.rpcs[uid].videoElement);
}
} catch (err) {
errorlog(err);
}
});
rpc.viewChromaCanvas = canvas;
rpc.viewChromaCanvasCtx = canvas.getContext("2d", { alpha: true, willReadFrequently: true });
return rpc.viewChromaCanvas;
}
function hideIncomingViewChromaSourceVideo(UUID) {
if (!session.rpcs || !session.rpcs[UUID] || !session.rpcs[UUID].videoElement) {
return;
}
if (!session.viewChromaHideSource) {
restoreIncomingViewChromaSourceVideo(UUID);
return;
}
var video = session.rpcs[UUID].videoElement;
// The mixer uses inline opacity==="0" as a "skip this element" sentinel and
// unconditionally writes visibility="visible" on every pass. Touching those
// inline styles either drops the container from the DOM (taking the canvas
// with it) or gets clobbered on the next mixer tick. A class with !important
// survives both. pointer-events stays on the class too so clicks reach the canvas.
video.classList.add("viewchroma-source-hidden");
video.dataset.viewChromaSourceHidden = "true";
}
function restoreIncomingViewChromaSourceVideo(UUID) {
if (!session.rpcs || !session.rpcs[UUID] || !session.rpcs[UUID].videoElement) {
return;
}
var video = session.rpcs[UUID].videoElement;
video.classList.remove("viewchroma-source-hidden");
delete video.dataset.viewChromaSourceHidden;
if (video.viewChromaSourceStyle) {
// Legacy inline-hide fallback: restore anything a prior build wrote onto style.
var style = video.viewChromaSourceStyle;
video.style.display = style.display || "";
video.style.visibility = style.visibility || "";
video.style.position = style.position || "";
video.style.inset = style.inset || "";
video.style.left = style.left || "";
video.style.top = style.top || "";
video.style.width = style.width || "";
video.style.height = style.height || "";
video.style.opacity = style.opacity || "";
video.style.pointerEvents = style.pointerEvents || "";
video.style.zIndex = style.zIndex || "";
video.viewChromaSourceStyle = null;
}
}
function mountIncomingViewChromaCanvas(UUID) {
if (!session.rpcs || !session.rpcs[UUID] || !session.rpcs[UUID].viewChromaCanvas || !session.rpcs[UUID].videoElement) {
return false;
}
var rpc = session.rpcs[UUID];
var canvas = rpc.viewChromaCanvas;
var video = rpc.videoElement;
var container = video.container || canvas.container || null;
var holder = video.holder || canvas.holder || (container && container.holder) || null;
if (!holder) {
return false;
}
if (canvas.parentNode !== holder) {
holder.appendChild(canvas);
}
canvas.container = container;
canvas.holder = holder;
canvas.style.position = "absolute";
canvas.style.left = "0";
canvas.style.top = "0";
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.style.maxWidth = "100%";
canvas.style.maxHeight = "100%";
canvas.style.objectFit = video.style.objectFit || "contain";
canvas.style.zIndex = "1";
return true;
}
function syncIncomingViewChromaCanvas(UUID) {
if (!session.rpcs[UUID] || !session.rpcs[UUID].viewChromaCanvas || !session.rpcs[UUID].videoElement) {
return false;
}
var rpc = session.rpcs[UUID];
var canvas = rpc.viewChromaCanvas;
var video = rpc.videoElement;
var sourceStyle = video.viewChromaSourceStyle || false;
canvas.id = "viewchroma_" + (video.id || UUID);
canvas.dataset.UUID = video.dataset.UUID || UUID;
if (video.dataset.sid || rpc.streamID) {
canvas.dataset.sid = video.dataset.sid || rpc.streamID;
}
if (video.dataset.aspectRatio) {
canvas.dataset.aspectRatio = video.dataset.aspectRatio;
} else if ("aspectRatio" in canvas.dataset) {
delete canvas.dataset.aspectRatio;
}
if (video.dataset.rotated) {
canvas.dataset.rotated = video.dataset.rotated;
} else if ("rotated" in canvas.dataset) {
delete canvas.dataset.rotated;
}
canvas.style.display = sourceStyle ? sourceStyle.display || video.style.display || "" : video.style.display || "";
canvas.style.visibility = sourceStyle ? sourceStyle.visibility || "" : video.style.visibility || "";
canvas.style.opacity = sourceStyle ? sourceStyle.opacity || "" : video.style.opacity || "";
canvas.style.transform = video.style.transform || "";
canvas.style.filter = video.style.filter || "";
canvas.style.pointerEvents = sourceStyle ? sourceStyle.pointerEvents || "auto" : video.style.pointerEvents || "auto";
canvas.order = typeof video.order === "number" ? video.order : 0;
canvas.title = video.title || "";
canvas.container = video.container || video.parentNode || null;
canvas.holder = video.holder || null;
canvas.proxyVideoElement = video;
canvas.srcObject = video.srcObject || null;
canvas.videoWidth = video.videoWidth || canvas.width || 0;
canvas.videoHeight = video.videoHeight || canvas.height || 0;
canvas.controls = !!video.controls;
mountIncomingViewChromaCanvas(UUID);
if (!canvas.clearDrawOnVideo && video.clearDrawOnVideo) {
canvas.clearDrawOnVideo = video.clearDrawOnVideo;
}
if (video.classList.contains("task")) {
canvas.classList.add("task");
} else {
canvas.classList.remove("task");
}
if (video.classList.contains("fadein")) {
canvas.classList.add("fadein");
} else {
canvas.classList.remove("fadein");
}
return true;
}
function processIncomingViewChromaFrame(UUID) {
if (!session.rpcs[UUID] || !session.rpcs[UUID].viewChromaState || !session.rpcs[UUID].videoElement) {
return false;
}
var rpc = session.rpcs[UUID];
var state = rpc.viewChromaState;
var video = rpc.videoElement;
if (!video.container || !video.container.holder || !document.body.contains(video.container.holder)) {
return false;
}
if (video.readyState < 2 || !video.videoWidth || !video.videoHeight) {
return false;
}
var firstFrame = !state.hasFrame;
var canvas = createIncomingViewChromaCanvas(UUID);
var ctx = rpc.viewChromaCanvasCtx;
if (!canvas || !ctx) {
return false;
}
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
}
try {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
applyColorKeyToImageData(imageData, state.config);
ctx.putImageData(imageData, 0, 0);
} catch (e) {
errorlog(e);
stopIncomingViewChroma(UUID, true);
return false;
}
state.hasFrame = true;
if (firstFrame && state.timer) {
clearTimeout(state.timer);
state.timer = null;
}
syncIncomingViewChromaCanvas(UUID);
if (mountIncomingViewChromaCanvas(UUID)) {
hideIncomingViewChromaSourceVideo(UUID);
}
return true;
}
function scheduleIncomingViewChroma(UUID) {
if (!session.rpcs[UUID] || !session.rpcs[UUID].viewChromaState || session.rpcs[UUID].viewChromaState.stopped) {
return;
}
var rpc = session.rpcs[UUID];
var state = rpc.viewChromaState;
var video = rpc.videoElement;
if (!video) {
return;
}
if (typeof video.requestVideoFrameCallback === "function") {
if (state.videoFrameHandle !== null && state.videoFrameHandle !== undefined) {
return;
}
state.videoFrameHandle = video.requestVideoFrameCallback(function () {
state.videoFrameHandle = null;
if (!session.rpcs[UUID] || !session.rpcs[UUID].viewChromaState || session.rpcs[UUID].viewChromaState.stopped) {
return;
}
processIncomingViewChromaFrame(UUID);
scheduleIncomingViewChroma(UUID);
});
}
if (state.timer || (typeof video.requestVideoFrameCallback === "function" && state.hasFrame)) {
return;
}
state.timer = setTimeout(function () {
state.timer = null;
if (!session.rpcs[UUID] || !session.rpcs[UUID].viewChromaState || session.rpcs[UUID].viewChromaState.stopped) {
return;
}
if (!processIncomingViewChromaFrame(UUID)) {
scheduleIncomingViewChroma(UUID);
return;
}
scheduleIncomingViewChroma(UUID);
}, 50);
}
function stopIncomingViewChroma(UUID, removeCanvas = true) {
if (!session.rpcs || !session.rpcs[UUID]) {
return;
}
var rpc = session.rpcs[UUID];
var state = rpc.viewChromaState;
if (state) {
state.stopped = true;
if (state.timer) {
clearTimeout(state.timer);
state.timer = null;
}
if (
rpc.videoElement &&
typeof rpc.videoElement.cancelVideoFrameCallback === "function" &&
state.videoFrameHandle !== null &&
state.videoFrameHandle !== undefined
) {
try {
rpc.videoElement.cancelVideoFrameCallback(state.videoFrameHandle);
} catch (e) {}
}
}
if (removeCanvas && rpc.viewChromaCanvas) {
try {
rpc.viewChromaCanvas.remove();
} catch (e) {}
rpc.viewChromaCanvas = null;
rpc.viewChromaCanvasCtx = null;
rpc.viewChromaState = null;
restoreIncomingViewChromaSourceVideo(UUID);
} else if (rpc.viewChromaCanvas) {
rpc.viewChromaCanvas.style.display = "none";
}
}
function configureIncomingViewChroma(UUID) {
if (!shouldApplyIncomingViewChroma(UUID)) {
stopIncomingViewChroma(UUID, true);
return false;
}
var rpc = session.rpcs[UUID];
var config = getIncomingViewChromaConfig();
if (!config) {
stopIncomingViewChroma(UUID, true);
return false;
}
if (!rpc.viewChromaState || rpc.viewChromaState.config.cacheKey !== config.cacheKey) {
stopIncomingViewChroma(UUID, false);
rpc.viewChromaState = {
config: config,
hasFrame: false,
stopped: false,
videoFrameHandle: null,
timer: null
};
} else {
rpc.viewChromaState.config = config;
rpc.viewChromaState.stopped = false;
}
scheduleIncomingViewChroma(UUID);
return true;
}
function getRenderedRemoteElement(UUID) {
if (!session.rpcs || !session.rpcs[UUID]) {
return false;
}
var rpc = session.rpcs[UUID];
if (!session.director && rpc.viewChromaCanvas && rpc.viewChromaState && rpc.viewChromaState.hasFrame) {
syncIncomingViewChromaCanvas(UUID);
return rpc.viewChromaCanvas;
}
return rpc.videoElement || false;
}
function resolveMediaContextElement(mediaElement) {
if (mediaElement && mediaElement.proxyVideoElement) {
return mediaElement.proxyVideoElement;
}
return mediaElement;
}
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 getLoudnessCallbackID() {
if (!session) {
return null;
}
if (typeof session.pushLoudnessCIB !== "undefined" && session.pushLoudnessCIB !== null) {
return session.pushLoudnessCIB;
}
return null;
}
function postLoudnessToIframe(loudnessObj, value, UUID = null, mode = "update") {
if (!isIFrame) {
return true;
}
if (!session || session.pushLoudness !== true) {
return true;
}
var loudnessMessage = {
loudness: loudnessObj,
action: "loudness",
mode: mode || "update",
value: value
};
if (UUID) {
loudnessMessage.UUID = UUID;
}
var loudnessCIB = getLoudnessCallbackID();
if (loudnessCIB !== null) {
loudnessMessage.cib = loudnessCIB;
}
try {
parent.postMessage(loudnessMessage, session.iframetarget);
return true;
} catch (e) {
return false;
}
}
function ensureLoudnessPipeline(UUID, reason = "unknown") {
try {
if (!session || !session.rpcs || !session.rpcs[UUID]) {
return false;
}
if (session.disableViewerWebAudioPipeline) {
return false;
}
var rpc = session.rpcs[UUID];
if (!rpc.streamSrc || !rpc.videoElement) {
return false;
}
if (!rpc.inboundAudioPipeline) {
rpc.inboundAudioPipeline = {};
}
var tracks = rpc.streamSrc.getAudioTracks();
if (!tracks.length) {
return false;
}
var now = Date.now();
var trackid = tracks[0].id;
var pipeline = rpc.inboundAudioPipeline[trackid];
if (pipeline && pipeline.analyser) {
var heartbeatWindowMs = 3000;
var warmupWindowMs = 1500;
var loudnessLastTickAt = parseInt(pipeline.loudnessLastTickAt) || 0;
var loudnessStartedAt = parseInt(pipeline.loudnessStartedAt) || 0;
if (loudnessLastTickAt && (now - loudnessLastTickAt) < heartbeatWindowMs) {
return true;
}
if (!loudnessLastTickAt && loudnessStartedAt && (now - loudnessStartedAt) < warmupWindowMs) {
return true;
}
}
if (!rpc.loudnessRecoveryState) {
rpc.loudnessRecoveryState = {
attempts: [],
nextAttemptAt: 0,
blockedUntil: 0,
lastBlockedLog: 0
};
}
var recoveryState = rpc.loudnessRecoveryState;
if (recoveryState.blockedUntil && now < recoveryState.blockedUntil) {
return false;
}
if (recoveryState.nextAttemptAt && now < recoveryState.nextAttemptAt) {
return false;
}
recoveryState.attempts = recoveryState.attempts.filter(function (ts) {
return now - ts < 30000;
});
if (recoveryState.attempts.length >= 3) {
recoveryState.blockedUntil = now + 10000;
recoveryState.nextAttemptAt = recoveryState.blockedUntil;
if (!recoveryState.lastBlockedLog || now - recoveryState.lastBlockedLog > 3000) {
warnlog("loudness recovery paused for " + UUID + " after repeated rebuild attempts");
recoveryState.lastBlockedLog = now;
}
return false;
}
recoveryState.attempts.push(now);
recoveryState.nextAttemptAt = now + 2000;
recoveryState.lastReason = reason || "unknown";
updateIncomingAudioElement(UUID);
return true;
} catch (e) {
warnlog(e);
return false;
}
}
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) {
var roomOnlyTierChanged = false;
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 ("rot" in miniInfo) {
var roomOnlyTier = parseInt(miniInfo.rot) || 0;
if (session.rpcs[UUID].roomOnlyTier !== roomOnlyTier) {
roomOnlyTierChanged = true;
}
session.rpcs[UUID].roomOnlyTier = roomOnlyTier;
session.rpcs[UUID].stats.info.room_only_tier = roomOnlyTier;
}
if (roomOnlyTierChanged) {
updateMixer();
}
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].loudnessStartedAt = Date.now();
session.rpcs[UUID].inboundAudioPipeline[trackid].loudnessLastTickAt = 0;
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);
session.rpcs[UUID].canvasIntervalAction = null;
var canvasIntervalAction = setInterval(
function (uuid) {
if (session.style !== 2) {
clearInterval(canvasIntervalAction); // this is FFT only, so okay to kill.
if (session.rpcs[uuid]) {
session.rpcs[uuid].canvasIntervalAction = null;
}
return;
}
try {
session.rpcs[uuid].inboundAudioPipeline[trackid].loudnessLastTickAt = Date.now();
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;
postLoudnessToIframe(loudnessObj, loudness, uuid);
}
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);
if (session.rpcs[uuid]) {
session.rpcs[uuid].canvasIntervalAction = null;
}
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();
session.rpcs[UUID].inboundAudioPipeline[trackid].loudnessStartedAt = Date.now();
session.rpcs[UUID].inboundAudioPipeline[trackid].loudnessLastTickAt = 0;
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);
var meterTickDelay = 100;
var maxMeterTickDelay = 2000;
var consecutiveErrors = 0;
var recoveryAttempts = 0;
var pauseUntil = 0;
var logEveryNErrors = 10;
function trackStillActive() {
if (!session.rpcs[UUID] || !session.rpcs[UUID].streamSrc) {
return false;
}
var tracks = session.rpcs[UUID].streamSrc.getAudioTracks();
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].id == trackid && tracks[i].readyState !== "ended") {
return true;
}
}
return false;
}
function scheduleNextTick(delayOverride = null) {
try {
if (!session.rpcs[UUID] || !session.rpcs[UUID].inboundAudioPipeline || !session.rpcs[UUID].inboundAudioPipeline[trackid] || !session.rpcs[UUID].inboundAudioPipeline[trackid].analyser) {
return;
}
clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
var delay = meterTickDelay;
if (delayOverride !== null && !isNaN(delayOverride)) {
delay = Math.max(25, parseInt(delayOverride));
}
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () {
updateLevels();
}, delay);
} catch (e) {
// no-op: audio path is being torn down
}
}
function updateLevels() {
var shouldReschedule = true;
var nextDelayOverride = null;
try {
var now = Date.now();
if (pauseUntil && now < pauseUntil) {
nextDelayOverride = pauseUntil - now;
return;
}
if (!session.rpcs[UUID]) {
shouldReschedule = false;
return;
}
if (!session.rpcs[UUID].inboundAudioPipeline || !session.rpcs[UUID].inboundAudioPipeline[trackid] || !session.rpcs[UUID].inboundAudioPipeline[trackid].analyser) {
shouldReschedule = false;
return;
}
if (!trackStillActive()) {
shouldReschedule = false;
return;
}
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteFrequencyData(dataArray);
session.rpcs[UUID].inboundAudioPipeline[trackid].loudnessLastTickAt = Date.now();
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 (!postLoudnessToIframe(loudnessObj, session.rpcs[UUID].stats.Audio_Loudness, UUID)) {
throw new Error("Unable to post loudness update");
}
}
consecutiveErrors = 0;
recoveryAttempts = 0;
pauseUntil = 0;
meterTickDelay = 100;
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) {
consecutiveErrors += 1;
if (meterTickDelay < 250) {
meterTickDelay = 250;
} else {
meterTickDelay = Math.min(maxMeterTickDelay, meterTickDelay * 2);
}
if (consecutiveErrors === 1 || !(consecutiveErrors % logEveryNErrors)) {
warnlog("audioMeterGuest update error #" + consecutiveErrors + " for " + UUID + ":" + trackid);
warnlog(e);
}
if (session.pushLoudness && typeof ensureLoudnessPipeline === "function") {
recoveryAttempts += 1;
if (!(recoveryAttempts % 3)) {
ensureLoudnessPipeline(UUID, "audioMeterGuest-error");
}
}
if (consecutiveErrors >= 30) {
pauseUntil = Date.now() + 10000;
consecutiveErrors = 0;
recoveryAttempts = 0;
meterTickDelay = 1000;
}
} finally {
if (!shouldReschedule) {
return;
}
scheduleNextTick(nextDelayOverride);
}
}
scheduleNextTick(100);
return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser;
}
async function effectsDynamicallyUpdate(event, ele) {
log("effectsDynamicallyUpdate");
let lastEffectValue = session.effect;
session.effect = ele.options[ele.selectedIndex].value;
// Restore saved effect value for this session context if available
try {
var savedVal = await getSavedEffectValue(session.effect);
if (savedVal !== null) {
session.effectValue_default = savedVal;
}
} catch(e) {}
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" || session.effect === "16") {
if (!["3", "4", "5", "16"].includes(lastEffectValue)) {
attemptSegmentationEffectModelLoad();
if (!(session.tfliteModule && 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", "16"].includes(lastEffectValue)) {
attemptSegmentationEffectModelLoad();
if (!(session.tfliteModule && 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.