/*
* Copyright (c) 2024 Steve Seguin. All Rights Reserved.
*
* Use of this source code is governed by the APGLv3 open-source license
* that can be found in the LICENSE file in the root of the source
* tree. Alternative licencing options can be made available on request.
*
*/
/*jshint esversion: 6 */
///// For the debug output, uncomment this section.
/*
let lastLogTime = performance.now(); // Initialize with the current time
function getTimeStamp() {
const now = performance.now();
const timeSinceLastLog = now - lastLogTime;
lastLogTime = now; // Update lastLogTime to the current time
return timeSinceLastLog.toFixed(0); // Return time with three decimals for milliseconds
}
function getStackTrace() {
const obj = {};
Error.captureStackTrace(obj, getStackTrace);
return obj.stack;
}
function getLineNumber() {
const e = new Error();
const frame = e.stack.split("\n")[3]; // Change the index if needed
const lineNumber = frame.split(":").reverse()[1];
return lineNumber;
}
function log(msg) {
const timeStamp = getTimeStamp();
const lineNumber = getLineNumber();
console.log(`${timeStamp}ms [Line ${lineNumber}]:`, msg);
}
function warnlog(msg) {
const timeStamp = getTimeStamp();
const lineNumber = getLineNumber();
console.warn(`${timeStamp}ms [Line ${lineNumber}]:`, msg);
}
function errorlog(msg) {
const timeStamp = getTimeStamp();
const lineNumber = getLineNumber();
console.error(`${timeStamp}ms [Line ${lineNumber}]:`, msg);
}
*/
///////
var formSubmitting = true;
var activatedPreview = false;
var screensharesupport = true;
var FirefoxEnumerated = false;
var Callbacks = [];
var CtrlPressed = false; // global
var MousePressed = false; // global
var AltPressed = false;
var KeyPressedTimeout = 0;
var PPTKeyPressed = false;
var translation = false;
var miscTranslations = {
// i can replace this list from time to time from the generated one in blank.json using translate.js
start: "START",
"new-display-name": "Enter a new Display Name for this stream",
"submit-error-report": "Press OK to submit any error logs to VDO.Ninja. Error logs may contain private information.",
"director-redirect-1": "The director wishes to redirect you to the URL: ",
"director-redirect-2": "\n\nPress OK to be redirected.",
"add-a-label": "Add a label",
"audio-processing-disabled": "Audio processing is disabled with this guest. Can't mute or change volume",
"not-the-director": "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("-", " "); //
}
}
if (typeof session === "undefined") {
// make sure to init the WebRTC if not exists.
var session = WebRTC.Media;
session.streamID = session.generateStreamID();
errorlog("Serious error: WebRTC session didn't load in time");
}
try {
// this is just in case orientationchange gets removed..
if (!window.onorientationchange && screen.orientation) {
// onorientationchange is deprecated.
window.onorientationchange = function () {
log("screen.orientation triggered.. but nothing linked");
};
screen.orientation.addEventListener("change", window.onorientationchange);
}
} catch (e) {
errorlog(e);
}
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
searchString = searchString.replace("??", "?");
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[?&]" + name + "=([^]*)").exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
var urlParams = new URLSearchParams(urlEdited);
if (urlParams.has("invite") || urlParams.has("i") || urlParams.has("code")) {
session.decodeInvite(urlParams.get("invite") || urlParams.get("i") || urlParams.get("code"));
} else if (urlParams.has("preset")) {
try {
let preset = urlParams.get("preset") || "1"; // default to preset 1 if none provided
let xhttp = new XMLHttpRequest();
xhttp.open("GET", "presets.json", false); // blocking
xhttp.setRequestHeader("Content-Type", "application/json"); // expecting json response
xhttp.send();
if (xhttp.status === 200) {
const response = JSON.parse(xhttp.responseText);
let presetString = "";
if (Array.isArray(response)) {
let index = (parseInt(preset) || 1) - 1;
presetString = response[index];
} else if (typeof response === "object" && response !== null) {
presetString = response[preset];
}
if (!presetString.startsWith("?") || presetString.startsWith("&")) {
presetString = "?" + presetString;
}
session.preset = presetString;
let newURL = presetString + "&" + urlParams.toString();
newURL = newURL.replace(/\?/g, "&");
newURL = newURL.replace(/\&/, "?");
urlParams = new URLSearchParams(newURL);
if (urlParams.has("invite") || urlParams.has("i") || urlParams.has("code")) {
session.decodeInvite(urlParams.get("invite") || urlParams.get("i") || urlParams.get("code"));
}
} else {
errorlog(xhttp.statusTex);
}
} catch (error) {
errorlog(error);
}
}
if (session.decrypted) {
session.decrypted = session.decrypted + urlEdited.replace("?", "&");
session.decrypted = session.decrypted.replace(/\?/g, "&");
session.decrypted = session.decrypted.replace(/\&/, "?");
urlParams = new URLSearchParams(session.decrypted);
//session.decrypted = true;
} else if (urlEdited !== window.location.search) {
warnlog(window.location.search + " changed to " + urlEdited);
if (!session.nohistory){
window.history.pushState({ path: urlEdited.toString() }, "", urlEdited.toString());
}
}
delete urlEdited;
var isIFrame = false;
if (parent && window.location !== window.parent.location) {
isIFrame = true;
}
function mapToAll(targets, callback, parentElement = document) {
// js helper
if (!targets) {
return;
}
if (!parentElement) {
return;
}
const target = parentElement.querySelectorAll(targets);
for (let i = 0; i < target.length; i++) {
callback(target[i]);
}
}
function changeParam(url, paramName, paramValue) {
paramName = paramName.replace("?", "");
var qind = url.indexOf("?");
url = url.replace("?", "&");
var params = url.substring(qind + 1).split("&");
var query = "";
var match = false;
for (var i = 0; i < params.length; i++) {
var tokens = params[i].split("=");
var name = tokens[0];
var value = "";
if (tokens.length > 1 && tokens[1] !== "") {
value = tokens[1];
}
if (name == paramName) {
if (match) {
continue;
} // already matched the first time.
match = true;
value = paramValue;
}
if (value !== "") {
value = "=" + value;
}
if (query == "") {
query = "?" + name + value;
} else {
query = query + "&" + name + value;
}
}
return url.substring(0, qind) + query;
}
function saveRoom(ele) {
//this.title = "Quick load settings stored locally";
session.sticky = true;
ele.parentNode.removeChild(ele);
setStorage("permission", "yes");
setStorage("settings", encodeURI(window.location.href), 999);
}
function updateURL(param, force = false, cleanUrl = false) {
if (session.decrypted) {
return;
}
param = param.replace("?", "");
var para = param.split("=");
if (cleanUrl) {
if (history.pushState) {
var href = new URL(cleanUrl);
if (para.length == 1) {
href = changeParam(cleanUrl, para[0], "");
} else {
href = changeParam(cleanUrl, para[0], para[1]);
}
log("--" + href.toString());
if (!session.nohistory){
window.history.pushState({ path: href.toString() }, "", href.toString());
}
}
} else if (!urlParams.has(para[0])) {
// don't need to replace as it doesn't exist.
if (history.pushState) {
var href = window.location.href;
href = href.replace("??", "?");
var arr = href.split("?");
var newurl;
if (arr.length > 1 && arr[1] !== "") {
newurl = href + "&" + param;
} else {
newurl = href + "?" + param;
}
if (!session.nohistory){
window.history.pushState({ path: newurl.toString() }, "", newurl.toString());
}
}
} else if (force) {
if (history.pushState) {
var href = new URL(window.location.href);
if (para.length == 1) {
href = changeParam(window.location.href, para[0], "");
} else {
href = changeParam(window.location.href, para[0], para[1]);
}
log("---" + href.toString());
if (!session.nohistory){
window.history.pushState({ path: href.toString() }, "", href.toString());
}
}
}
if (session.sticky) {
setStorage("settings", encodeURI(window.location.href), 999);
}
urlParams = new URLSearchParams(window.location.search);
if (session.preset) {
let newURL = session.preset + "&" + urlParams.toString();
newURL = newURL.replace(/\?/g, "&");
newURL = newURL.replace(/\&/, "?");
urlParams = new URLSearchParams(newURL);
}
}
/* function changeGuestSettings(ele){
var eles = ele.querySelectorAll('[data-param]');
var UUID = ele.dataset.UUID;
var settings = {};
for (var i = 0;i< eles.length; i++){
if (eles[i].tagName.toLowerCase() == "input"){
if (eles[i].checked===true){
settings[eles[i].dataset.param] = true;
} else if (eles[i].checked===false){
settings[eles[i].dataset.param] = false;
} else {
settings[eles[i].dataset.param] = eles[i].value;
}
}
}
warnlog(settings);
if (!settings.changepassword){
delete settings.password;
}
delete settings.changepassword;
if (!settings.changeroom){
// send Migration message
delete settings.roomid;
}
delete settings.roomid;
delete settings.changeroom;
warnlog(UUID);
var msg = {};
msg.changeParams = settings;
session.sendRequest(msg, UUID);
closeModal();
} */
// proper room migration needs to happen; in sync.
// updateMixer after settings changed
// password needs to be special cased
// room shouldn't be sent
function applyNewParams(changeParams) {
for (var key in changeParams) {
session[key] = changeParams[key];
log(key);
}
log(changeParams);
updateMixer();
}
function submitDebugLog(msg = false) {
try {
if (navigator.userAgent) {
var _,
userAgent = navigator.userAgent;
appendDebugLog({ userAgent: userAgent });
}
if (navigator.platform) {
appendDebugLog({ userAgent: navigator.platform });
}
} catch (e) {}
window.focus();
var res = confirm(getTranslation("submit-error-report"));
if (res) {
var request = new XMLHttpRequest();
var recordResults = session.streamID + "_" + parseInt(Date.now());
request.open("POST", "https://reports.vdo.ninja/?name=" + recordResults); // php, well, whatever.
if (!session.cleanOutput) {
warnUser("Report any details of your bug report to steve@seguin.email, along with the following link: https://reports.vdo.ninja/?name=" + recordResults + "", false, false);
}
console.log("Report any details of your bug report to steve@seguin.email, along with the following ID: " + recordResults);
request.send(JSON.stringify(errorReport));
errorReport = [];
if (document.getElementById("reportbutton")) {
getById("reportbutton").classList.add("hidden");
}
}
}
function URLFromFiles(files) {
const promises = files.map(file => fetch(file).then(response => response.text()));
return Promise.all(promises).then(texts => {
const text = texts.join("");
const blob = new Blob([text], { type: "application/javascript" });
return URL.createObjectURL(blob);
});
}
function detectCPUSupport() {
let cpuThreads = navigator.hardwareConcurrency;
if (cpuThreads) {
return cpuThreads + " threads";
}
return false;
}
function detectGPUSupport() {
try {
const gl = document.createElement("canvas").getContext("webgl");
if (!gl) {
return false;
}
if (!Firefox) {
try {
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); // chrome
if (debugInfo) {
return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
}
} catch (e) {}
}
try {
return gl.getParameter(gl.RENDERER) || false; // firefox
} catch (e) {}
} catch (e) {}
return false;
}
function isOperaGX() {
return (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(" OPR/75") >= 0;
}
function isSamsungASeries() {
return navigator.userAgent.includes("; SM-A") || false;
}
function getChromiumVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
function getiOSVersion() {
try {
var agent = navigator.userAgent;
var start = agent.indexOf("OS ");
if ((agent.indexOf("iPhone") > -1 || agent.indexOf("iPad") > -1) && start > -1) {
return window.Number(agent.substr(start + 3, 3).replace("_", "."));
}
return 0;
} catch (e) {
return 0;
}
return 0;
}
function safariVersion() {
var ver = 0;
try {
ver = navigator.appVersion.split("Version/");
if (ver.length > 1) {
ver = ver[1].split(" Safari");
}
if (ver.length > 1) {
ver = ver[0].split(".");
}
if (ver.length > 1) {
ver = parseInt(ver[0]);
} else {
ver = 0;
}
} catch (e) {
return 0;
}
return ver;
}
function isIntelMac() {
// Check if it's a Mac but not Apple Silicon
if (macOS && navigator.userAgent.indexOf("Intel") >= 0) {
return true;
}
return false;
}
function judgePerformance(){
try {
if (SafariVersion && SafariVersion >= 17 && (iOS || iPad)) { // iphone xr or newer
return 0;
}
const cores = typeof navigator.hardwareConcurrency === 'number' ? navigator.hardwareConcurrency : 0;
if (isIntelMac()) {
if (cores < 6) { // yes. they are that bad.
return 2;
} else {
return 1;
}
}
if (session.mobile && (cores>=4)){ // assume hardware encoded acceleration
return 0;
}
if (!cores){
return 1;
} else if (cores < 4 ){
return 2;
} else if (cores>8){
return 0;
}
return 1
} catch (e) {
return 1; // 99% safe default
}
}
try {
var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); // used by main.js also
var iPad = navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
var macOS = navigator.userAgent.indexOf("Mac OS X") != -1;
macOS = macOS && !(iOS || iPad);
var Firefox = navigator.userAgent.indexOf("Firefox") >= 0;
if (Firefox) {
Firefox = parseInt(navigator.userAgent.split("irefox/").pop()) || true;
}
var Android = navigator.userAgent.toLowerCase().indexOf("android") > -1; //&& ua.indexOf("mobile");
var ChromiumVersion = getChromiumVersion();
var OperaGx = isOperaGX();
var SafariVersion = safariVersion() || getiOSVersion(); // I should rename this to webkit
if (iOS || iPad) {
// iOS doesn't yet allow actual browsers, cause it's abusing its duopoly.
if (SafariVersion) {
if (Firefox) {
Firefox = false; // I should rename this to gecko
}
if (ChromiumVersion) {
ChromiumVersion = false; // I should rename this to chromium
}
}
}
var SamsungASeries = isSamsungASeries();
var isVingester = navigator.userAgent.indexOf("Vingester") >= 0;
var gpgpuSupport = detectGPUSupport(); // graphics ; not supported on ios
log(gpgpuSupport);
var cpuSupport = detectCPUSupport(); // thread count ; supported on ios
log(cpuSupport);
var iPhone12Up = false;
var isMELD = false;
if (typeof navigator!== 'undefined' && navigator.userAgent && navigator.userAgent.includes("Meld/")) {
isMELD = true;
}
if (iOS && !iPad) {
if (window.devicePixelRatio.toFixed(2) >= 3 && window.screen.height > 800 && window.screen.width != 414) {
// for reference, https://www.ios-resolution.com/
iPhone12Up = true; // iPhone SE is left out.
}
}
session.quality_wb = judgePerformance(); // try to estimate what resolution to use for encoding when not in a room.
if (session.quality_room < session.quality_wb){
session.quality_room = session.quality_wb;
}
} catch (e) {
errorlog(e);
}
const needsLegacyWakeLock = () => {
try {
if ('wakeLock' in navigator) {
if (Firefox) { // using your existing Firefox detection
return true;
}
if ((iOS || iPad) && SafariVersion < 16.4) { // using your existing iOS/iPad/SafariVersion detection
return true;
}
return false;
}
} catch(e){}
return true; // No Wake Lock API, need legacy keep alive for mobile
};
if (session.audioCtx && session.audioCtx.sampleRate && session.audioCtx.sampleRate > 192000) {
console.warn("Your audio playback device has a very high sample-rate set of " + session.audioCtx.sampleRate + "-Hz. If having audio problems, lower to at least 192000-Hz, but preferably 48000-Hz.");
if (!session.cleanOutput) {
miniTranslate(getById("audioTipContextSR"), "sample-rate-too-high");
getById("audioTipSR").classList.remove("hidden");
}
}
if (isVingester) {
console.warn("If Vingester isn't able to capture audio, get a fixed version of Vingester from here: https://github.com/steveseguin/vingester/releases/");
}
function isAlphaNumeric(str) {
var code, i, len;
for (i = 0, len = str.length; i < len; i++) {
code = str.charCodeAt(i);
if (
!(code > 47 && code < 58) && // numeric (0-9)
!(code > 64 && code < 91) && // upper alpha (A-Z)
!(code > 96 && code < 123)
) {
// lower alpha (a-z)
return false;
}
}
return true;
}
function convertStringToArrayBufferView(str) {
var bytes = new Uint8Array(str.length);
for (var iii = 0; iii < str.length; iii++) {
bytes[iii] = str.charCodeAt(iii);
}
return bytes;
}
function toHexString(byteArray) {
return Array.prototype.map
.call(byteArray, function (byte) {
return ("0" + (byte & 0xff).toString(16)).slice(-2);
})
.join("");
}
function toByteArray(hexString) {
var result = [];
for (var i = 0; i < hexString.length; i += 2) {
result.push(parseInt(hexString.substr(i, 2), 16));
}
return new Uint8Array(result);
}
function playAllVideos() {
if (session.firstPlayTriggered && session.audioCtx.state == "suspended") {
// added oct 9th 2022
try {
session.audioCtx.resume();
} catch (e) {
warnlog(e);
}
}
for (var i in session.rpcs) {
if (session.rpcs[i].whip) {
continue;
}
try {
if (session.rpcs[i].videoElement) {
log("I: " + i);
if (session.rpcs[i].videoElement.paused) {
setTimeout(
function (UUID) {
session.rpcs[UUID].videoElement
.play()
.then(_ => {
log("playing 3 ");
if (session.audioEffects === true || session.pushLoudness) {
log("updateIncomingAudioElement('" + UUID + "')");
updateIncomingAudioElement(UUID);
}
})
.catch(errorlog);
},
0,
i
);
} else if (session.audioEffects === true || session.pushLoudness) {
updateIncomingAudioElement(i);
log("updateIncomingAudioElement('" + i + "')");
}
}
} catch (e) {
errorlog(e);
}
}
}
var videoElements = Array.from(document.querySelectorAll("video"));
var audioElements = Array.from(document.querySelectorAll("audio"));
var mediaStreamCounter = 0;
function createMediaStream() {
mediaStreamCounter += 1;
return new MediaStream();
}
function deleteOldMedia() {
log("CHECKING FOR OLD MEDIA");
var i = videoElements.length;
while (i--) {
//if ((videoElements[i].id == "videosource") || (videoElements[i].id == "previewWebcam")){continue;} // exclude this one, for safety reasons. (Also, iOS safari blanks the video if streams are detached and moved between video elements)
if (videoElements[i].isConnected === false) {
if (videoElements[i].srcObject == null || (videoElements[i].srcObject && videoElements[i].srcObject.active === false)) {
if (videoElements[i].dataset && videoElements[i].dataset.UUID) {
if (videoElements[i].dataset.UUID in session.rpcs) {
continue;
} // still active, so lets not delete it.
}
videoElements[i].pause();
videoElements[i].removeAttribute("id");
videoElements[i].removeAttribute("src"); // empty source
videoElements[i].load();
videoElements[i].remove();
videoElements[i] = null;
videoElements.splice(i, 1);
}
}
}
i = audioElements.length;
while (i--) {
if (audioElements[i].isConnected === false) {
if (audioElements[i].srcObject == null || (audioElements[i].srcObject && audioElements[i].srcObject.active === false)) {
if (audioElements[i].dataset && audioElements[i].dataset.UUID) {
if (audioElements[i].dataset.UUID in session.rpcs) {
continue;
} // still active, so lets not delete it.
}
audioElements[i].pause();
audioElements[i].id = null;
audioElements[i].removeAttribute("src"); // empty source
audioElements[i].load();
audioElements[i].remove();
audioElements[i] = null;
audioElements.splice(i, 1);
}
}
}
}
function createAudioElement() {
try {
deleteOldMedia();
} catch (e) {
errorlog(e);
}
var a = document.createElement("audio");
audioElements.push(a);
return a;
}
function compare_deltas(a, b) {
var aa = a.delta || 0;
var bb = b.delta || 0;
if (aa > bb) {
return 1;
}
if (aa < bb) {
return -1;
}
return 0;
}
async function fetchWithTimeout(URL, timeout = 8000) {
// ref: https://dmitripavlutin.com/timeout-fetch-request/
try {
const controller = new AbortController();
const timeout_id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(URL, { ...{ timeout: timeout }, signal: controller.signal });
clearTimeout(timeout_id);
return response;
} catch (e) {
errorlog(e);
return await fetch(URL); // iOS 11.x/12.0
}
}
function createVideoElement() {
try {
deleteOldMedia();
} catch (e) {
errorlog(e);
}
var v = document.createElement("video");
videoElements.push(v);
if (typeof session.volume == "number") {
v.volume = session.volume; // setting default volume
log("setting volume to manual");
}
return v;
}
function getTimezone() {
if (session.tz !== false) {
return session.tz;
}
const stdTimezoneOffset = () => {
var jan = new Date(0, 1);
var jul = new Date(6, 1);
return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
};
var today = new Date();
const isDstObserved = today => {
return today.getTimezoneOffset() < stdTimezoneOffset();
};
if (isDstObserved(today)) {
return today.getTimezoneOffset() + 60;
} else {
return today.getTimezoneOffset();
}
}
function promptUser(eleId, UUID = null) {
if (session.beepToNotify) {
playtone();
}
if (document.getElementById("modalBackdrop")) {
getById("promptModal").innerHTML = ""; // Delete modal
getById("promptModal").remove();
getById("modalBackdrop").innerHTML = ""; // Delete modal
getById("modalBackdrop").remove();
}
zindex = document.querySelectorAll("#promptModal").length + document.querySelectorAll(".alertModal").length;
modalTemplate = `
×
`;
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.queue is now set to false; hold without video
//getById("overlayMsgs").innerText = "";
closeModal(false, "123");
for (var UUID in session.pcs) {
if (session.pcs[UUID].needsPublishing) {
session.initialPublish(UUID); // let's starts publishing now
}
}
} else if (session.queueType == 4) {
// hold with video
//getById("overlayMsgs").innerText = "";
closeModal(false, "123");
}
getChatMessage(getTranslation("you-have-been-activated"), (label = false), (director = false), (overlay = true));
hideHomeCheck();
}
function youreWaitingToBeActivated() {
// getChatMessage( getTranslation("you-not-yet-activated"), label = false, director = false, overlay = true);
warnUser(getTranslation("you-not-yet-activated"), false, false, 123);
hideHomeCheck();
}
async function confirmAlt(inputText, block = false) {
var result = null;
if (session.beepToNotify) {
playtone();
}
await new Promise((resolve, reject) => {
var promptID = "pid_" + Math.random().toString(36).substr(2, 9);
Prompts[promptID] = {};
Prompts[promptID].resolve = resolve;
Prompts[promptID].reject = reject;
var zindex = 33 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length;
if (block) {
var backdropClass = "opaqueBackdrop";
} else {
var backdropClass = "modalBackdrop";
}
inputText = "" + inputText.replace("\n", " ") + "";
inputText = inputText.replace(/\n/g, " ");
modalTemplate = `
×${inputText}
`;
document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end
document.getElementById("submit_" + promptID).focus();
document.getElementById("submit_" + promptID).addEventListener("click", function (event) {
var pid = event.target.dataset.pid;
result = true;
getById("modalBackdrop_" + pid).remove();
getById("modal_" + pid).remove();
Prompts[pid].resolve();
});
document.getElementById("cancel_" + promptID).addEventListener("click", function (event) {
var pid = event.target.dataset.pid;
getById("modalBackdrop_" + pid).remove();
getById("modal_" + pid).remove();
Prompts[pid].resolve();
});
document.getElementById("close_" + promptID).addEventListener("click", function (event) {
var pid = event.target.dataset.pid;
getById("modalBackdrop_" + pid).remove();
getById("modal_" + pid).remove();
Prompts[pid].resolve();
});
getById("modal_" + promptID).addEventListener("click", function (e) {
e.stopPropagation();
return false;
});
miniTranslate(getById("modal_" + promptID));
return;
});
return result;
}
var modalTimeout = null;
function warnUser(message, timeout = false, sanitize = true, modalID = false) {
// Allows for multiple alerts to stack better.
// Every modal and backdrop has an increasing z-index
// to block the previous modal
if (!message) {
return;
}
if (document.getElementById("modalBackdrop")) {
getById("alertModal").innerHTML = ""; // Delete modal
getById("alertModal").remove();
getById("modalBackdrop").innerHTML = ""; // Delete modal
getById("modalBackdrop").remove();
}
zindex = 31 + document.querySelectorAll(".alertModal").length + document.querySelectorAll(".promptModal").length;
try {
if (sanitize) {
message = sanitizeChat(message, 2000);
}
message = message.replace(/\n/g, " ");
} 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) {
if (!document.querySelector("#alertModal[data-modalID='" + modalID + "']")) {
return;
}
}
clearTimeout(modalTimeout);
try {
getById("modalBackdrop").innerHTML = ""; // Delete modal
getById("modalBackdrop").remove();
getById("alertModal").innerHTML = ""; // Delete modal
getById("alertModal").remove();
getById("promptModal").innerHTML = ""; // Delete modal
getById("promptModal").remove();
query(".modalBackdrop").innerHTML = ""; // Delete modal
query(".modalBackdrop").remove();
if (ele && ele.innerHTML && ele.remove) {
ele.innerHTML = ""; // Delete specific modal
ele.remove();
}
} catch (e) {
warnlog(e);
}
if (session.timeoutTriggered) {
session.warnUserTriggered = false;
}
}
var sanitizeStreamID = function (streamID) {
streamID = streamID.trim();
if (streamID.length < 1) {
streamID = session.generateStreamID(8);
if (!session.cleanOutput) {
warnUser(getTranslation("no-streamID-provided") + streamID, false, false);
}
}
var streamID_sanitized = streamID.replace(/[\W]+/g, "_");
if (streamID !== streamID_sanitized) {
if (!session.cleanOutput) {
warnUser(getTranslation("alphanumeric-only"), false, false);
}
}
if (streamID_sanitized.length > 64) {
streamID_sanitized = streamID_sanitized.substring(0, 70); // leave room for salting
if (!session.cleanOutput) {
warnUser(getTranslation("stream-id-too-long"), false, false);
}
}
return streamID_sanitized;
};
var checkStrength = function (string) {
var matcher = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{7,30}$/;
if (string.match(matcher)) {
return true;
} else if (string.length > 20) {
return true;
} else {
return false;
}
};
var checkStrengthRoom = function () {
var result1 = checkStrength(getById("videoname1").value);
var result2 = getById("passwordRoom").value.length;
var target = getById("securityLevelRoom");
target.style.display = "block";
if (result1) {
if (result2) {
target.innerHTML = "" + getTranslation("share-with-trusted") + "";
} else {
target.innerHTML = "" + getTranslation("pass-recommended") + "";
}
} else {
target.innerHTML = "" + getTranslation("insecure-room-name") + " " + getTranslation("allowed-chars") + ": A-Z, a-z, 0-9, _";
}
};
var emojiShortCodes = {
":joy:": "😂",
":heart:": "❤️",
":heart_eyes:": "😍",
":sob:": "😭",
":blush:": "😊",
":unamused:": "😒",
":two_hearts:": "💕",
":weary:": "😩",
":ok_hand:": "👌",
":pensive:": "😔",
":smirk:": "😏",
":grin:": "😁",
":wink:": "😉",
":thumbsup:": "👍",
":pray:": "🙏",
":relieved:": "😌",
":notes:": "🎶",
":flushed:": "😳",
":raised_hands:": "🙌",
":see_no_evil:": "🙈",
":cry:": "😢",
":sunglasses:": "😎",
":v:": "✌️",
":eyes:": "👀",
":sweat_smile:": "😅",
":sparkles:": "✨",
":sleeping:": "😴",
":smile:": "😄",
":purple_heart:": "💜",
":broken_heart:": "💔",
":blue_heart:": "💙",
":confused:": "😕",
":disappointed:": "😞",
":yum:": "😋",
":neutral_face:": "😐",
":sleepy:": "😪",
":clap:": "👏",
":cupid:": "💘",
":heartpulse:": "💗",
":kiss:": "💋",
":point_right:": "👉",
":scream:": "😱",
":fire:": "🔥",
":rage:": "😡",
":smiley:": "😃",
":tada:": "🎉",
":tired_face:": "😫",
":camera:": "📷",
":rose:": "🌹",
":muscle:": "💪",
":skull:": "💀",
":sunny:": "☀️",
":yellow_heart:": "💛",
":triumph:": "😤",
":laughing:": "😆",
":sweat:": "😓",
":point_left:": "👈",
":grinning:": "😀",
":mask:": "😷",
":green_heart:": "💚",
":wave:": "👋",
":persevere:": "😣",
":heartbeat:": "💓",
":crown:": "👑",
":innocent:": "😇",
":headphones:": "🎧",
":confounded:": "😖",
":angry:": "😠",
":grimacing:": "😬",
":star2:": "🌟",
":gun:": "🔫",
":raising_hand:": "🙋",
":thumbsdown:": "👎",
":dancer:": "💃",
":musical_note:": "🎵",
":no_mouth:": "😶",
":dizzy:": "💫",
":fist:": "✊",
":point_down:": "👇",
":no_good:": "🙅",
":boom:": "💥",
":tongue:": "👅",
":poop:": "💩",
":cold_sweat:": "😰",
":gem:": "💎",
":ok_woman:": "🙆",
":pizza:": "🍕",
":joy_cat:": "😹",
":leaves:": "🍃",
":sweat_drops:": "💦",
":penguin:": "🐧",
":zzz:": "💤",
":walking:": "🚶",
":airplane:": "✈️",
":balloon:": "🎈",
":star:": "⭐",
":ribbon:": "🎀",
":worried:": "😟",
":underage:": "🔞",
":fearful:": "😨",
":hibiscus:": "🌺",
":microphone:": "🎤",
":open_hands:": "👐",
":ghost:": "👻",
":palm_tree:": "🌴",
":nail_care:": "💅",
":alien:": "👽",
":bow:": "🙇",
":cloud:": "☁",
":soccer:": "⚽",
":angel:": "👼",
":dancers:": "👯",
":snowflake:": "❄️",
":point_up:": "☝️",
":rainbow:": "🌈",
":gift_heart:": "💝",
":gift:": "🎁",
":beers:": "🍻",
":anguished:": "😧",
":earth_africa:": "🌍",
":movie_camera:": "🎥",
":anchor:": "⚓",
":zap:": "⚡",
":runner:": "🏃",
":sunflower:": "🌻",
":bouquet:": "💐",
":dog:": "🐶",
":moneybag:": "💰",
":herb:": "🌿",
":couple:": "👫",
":fallen_leaf:": "🍂",
":tulip:": "🌷",
":birthday:": "🎂",
":cat:": "🐱",
":coffee:": "☕",
":dizzy_face:": "😵",
":point_up_2:": "👆",
":open_mouth:": "😮",
":hushed:": "😯",
":basketball:": "🏀",
":ring:": "💍",
":astonished:": "😲",
":hear_no_evil:": "🙉",
":dash:": "💨",
":cactus:": "🌵",
":hotsprings:": "♨️",
":telephone:": "☎️",
":maple_leaf:": "🍁",
":princess:": "👸",
":massage:": "💆",
":love_letter:": "💌",
":trophy:": "🏆",
":blossom:": "🌼",
":lips:": "👄",
":fries:": "🍟",
":doughnut:": "🍩",
":frowning:": "😦",
":ocean:": "🌊",
":bomb:": "💣",
":cyclone:": "🌀",
":rocket:": "🚀",
":umbrella:": "☔",
":couplekiss:": "💏",
":lollipop:": "🍭",
":clapper:": "🎬",
":pig:": "🐷",
":smiling_imp:": "😈",
":imp:": "👿",
":bee:": "🐝",
":kissing_cat:": "😽",
":anger:": "💢",
":santa:": "🎅",
":earth_asia:": "🌏",
":football:": "🏈",
":guitar:": "🎸",
":panda_face:": "🐼",
":strawberry:": "🍓",
":smirk_cat:": "😼",
":banana:": "🍌",
":watermelon:": "🍉",
":snowman:": "⛄",
":smile_cat:": "😸",
":eggplant:": "🍆",
":crystal_ball:": "🔮",
":calling:": "📲",
":iphone:": "📱",
":partly_sunny:": "⛅",
":warning:": "⚠️",
":scream_cat:": "🙀",
":baby:": "👶",
":feet:": "🐾",
":footprints:": "👣",
":beer:": "🍺",
":wine_glass:": "🍷",
":video_camera:": "📹",
":rabbit:": "🐰",
":smoking:": "🚬",
":peach:": "🍑",
":snake:": "🐍",
":turtle:": "🐢",
":cherries:": "🍒",
":kissing:": "😗",
":frog:": "🐸",
":milky_way:": "🌌",
":closed_book:": "📕",
":candy:": "🍬",
":hamburger:": "🍔",
":bear:": "🐻",
":tiger:": "🐯",
":icecream:": "🍦",
":pineapple:": "🍍",
":ear_of_rice:": "🌾",
":syringe:": "💉",
":tv:": "📺",
":pill:": "💊",
":octopus:": "🐙",
":grapes:": "🍇",
":smiley_cat:": "😺",
":cd:": "💿",
":cocktail:": "🍸",
":cake:": "🍰",
":video_game:": "🎮",
":lipstick:": "💄",
":whale:": "🐳",
":cookie:": "🍪",
":dolphin:": "🐬",
":loud_sound:": "🔊",
":man:": "👨",
":monkey:": "🐒",
":books:": "📚",
":guardsman:": "💂",
":loudspeaker:": "📢",
":scissors:": "✂️",
":girl:": "👧",
":mortar_board:": "🎓",
":baseball:": "⚾️",
":woman:": "👩",
":fireworks:": "🎆",
":stars:": "🌠",
":mushroom:": "🍄",
":pouting_cat:": "😾",
":left_luggage:": "🛅",
":high_heel:": "👠",
":dart:": "🎯",
":swimmer:": "🏊",
":key:": "🔑",
":bikini:": "👙",
":family:": "👪",
":pencil2:": "✏",
":elephant:": "🐘",
":droplet:": "💧",
":seedling:": "🌱",
":apple:": "🍎",
":dollar:": "💵",
":book:": "📖",
":haircut:": "💇",
":computer:": "💻",
":bulb:": "💡",
":boy:": "👦",
":tangerine:": "🍊",
":sunrise:": "🌅",
":poultry_leg:": "🍗",
":shaved_ice:": "🍧",
":bird:": "🐦",
":eyeglasses:": "👓",
":goat:": "🐐",
":older_woman:": "👵",
":new_moon:": "🌑",
":customs:": "🛃",
":house:": "🏠",
":full_moon:": "🌕",
":lemon:": "🍋",
":baby_bottle:": "🍼",
":spaghetti:": "🍝",
":wind_chime:": "🎐",
":fish_cake:": "🍥",
":nose:": "👃",
":pig_nose:": "🐽",
":fish:": "🐟",
":koala:": "🐨",
":ear:": "👂",
":shower:": "🚿",
":bug:": "🐛",
":ramen:": "🍜",
":tophat:": "🎩",
":fuelpump:": "⛽",
":horse:": "🐴",
":watch:": "⌚",
":monkey_face:": "🐵",
":baby_symbol:": "🚼",
":sparkler:": "🎇",
":corn:": "🌽",
":tennis:": "🎾",
":battery:": "🔋",
":wolf:": "🐺",
":moyai:": "🗿",
":cow:": "🐮",
":mega:": "📣",
":older_man:": "👴",
":dress:": "👗",
":link:": "🔗",
":chicken:": "🐔",
":whale2:": "🐋",
":bento:": "🍱",
":pushpin:": "📌",
":dragon:": "🐉",
":hamster:": "🐹",
":golf:": "⛳",
":surfer:": "🏄",
":mouse:": "🐭",
":blue_car:": "🚙",
":bread:": "🍞",
":cop:": "👮",
":tea:": "🍵",
":bike:": "🚲",
":rice:": "🍚",
":radio:": "📻",
":baby_chick:": "🐤",
":sheep:": "🐑",
":lock:": "🔒",
":green_apple:": "🍏",
":racehorse:": "🐎",
":fried_shrimp:": "🍤",
":volcano:": "🌋",
":rooster:": "🐓",
":inbox_tray:": "📥",
":wedding:": "💒",
":sushi:": "🍣",
":ice_cream:": "🍨",
":tomato:": "🍅",
":rabbit2:": "🐇",
":beetle:": "🐞",
":bath:": "🛀",
":no_entry:": "⛔",
":crocodile:": "🐊",
":dog2:": "🐕",
":cat2:": "🐈",
":hammer:": "🔨",
":meat_on_bone:": "🍖",
":shell:": "🐚",
":poodle:": "🐩",
":stew:": "🍲",
":jeans:": "👖",
":honey_pot:": "🍯",
":unlock:": "🔓",
":black_nib:": "✒",
":snowboarder:": "🏂",
":white_flower:": "💮",
":necktie:": "👔",
":womens:": "🚺",
":ant:": "🐜",
":city_sunset:": "🌇",
":dragon_face:": "🐲",
":snail:": "🐌",
":dvd:": "📀",
":shirt:": "👕",
":game_die:": "🎲",
":dolls:": "🎎",
":8ball:": "🎱",
":bus:": "🚌",
":custard:": "🍮",
":camel:": "🐫",
":curry:": "🍛",
":hospital:": "🏥",
":bell:": "🔔",
":pear:": "🍐",
":door:": "🚪",
":saxophone:": "🎷",
":church:": "⛪",
":bicyclist:": "🚴",
":dango:": "🍡",
":office:": "🏢",
":rowboat:": "🚣",
":womans_hat:": "👒",
":mans_shoe:": "👞",
":love_hotel:": "🏩",
":mount_fuji:": "🗻",
":handbag:": "👜",
":hourglass:": "⌛",
":trumpet:": "🎺",
":school:": "🏫",
":cow2:": "🐄",
":toilet:": "🚽",
":pig2:": "🐖",
":violin:": "🎻",
":credit_card:": "💳",
":ferris_wheel:": "🎡",
":bowling:": "🎳",
":barber:": "💈",
":purse:": "👛",
":rat:": "🐀",
":date:": "📅",
":ram:": "🐏",
":tokyo_tower:": "🗼",
":kimono:": "👘",
":ship:": "🚢",
":mag_right:": "🔎",
":mag:": "🔍",
":fire_engine:": "🚒",
":police_car:": "🚓",
":black_joker:": "🃏",
":package:": "📦",
":calendar:": "📆",
":horse_racing:": "🏇",
":tiger2:": "🐅",
":boot:": "👢",
":ambulance:": "🚑",
":boar:": "🐗",
":pound:": "💷",
":ox:": "🐂",
":rice_ball:": "🍙",
":sandal:": "👡",
":tent:": "⛺",
":seat:": "💺",
":taxi:": "🚕",
":briefcase:": "💼",
":newspaper:": "📰",
":circus_tent:": "🎪",
":mens:": "🚹",
":flashlight:": "🔦",
":foggy:": "🌁",
":bamboo:": "🎍",
":ticket:": "🎫",
":helicopter:": "🚁",
":minidisc:": "💽",
":oncoming_bus:": "🚍",
":melon:": "🍈",
":notebook:": "📓",
":no_bell:": "🔕",
":oden:": "🍢",
":flags:": "🎏",
":blowfish:": "🐡",
":sweet_potato:": "🍠",
":ski:": "🎿",
":construction:": "🚧",
":satellite:": "📡",
":euro:": "💶",
":ledger:": "📒",
":leopard:": "🐆",
":truck:": "🚚",
":sake:": "🍶",
":railway_car:": "🚃",
":speedboat:": "🚤",
":vhs:": "📼",
":yen:": "💴",
":mute:": "🔇",
":wheelchair:": "♿",
":paperclip:": "📎",
":atm:": "🏧",
":telescope:": "🔭",
":rice_scene:": "🎑",
":blue_book:": "📘",
":postbox:": "📮",
":e-mail:": "📧",
":mouse2:": "🐁",
":nut_and_bolt:": "🔩",
":hotel:": "🏨",
":wc:": "🚾",
":green_book:": "📗",
":tractor:": "🚜",
":fountain:": "⛲",
":metro:": "🚇",
":clipboard:": "📋",
":no_smoking:": "🚭",
":slot_machine:": "🎰",
":bathtub:": "🛁",
":scroll:": "📜",
":station:": "🚉",
":rice_cracker:": "🍘",
":bank:": "🏦",
":wrench:": "🔧",
":bar_chart:": "📊",
":minibus:": "🚐",
":tram:": "🚊",
":microscope:": "🔬",
":bookmark:": "🔖",
":pouch:": "👝",
":fax:": "📠",
":sound:": "🔉",
":chart:": "💹",
":floppy_disk:": "💾",
":post_office:": "🏣",
":speaker:": "🔈",
":japan:": "🗾",
":mahjong:": "🀄",
":orange_book:": "📙",
":restroom:": "🚻",
":train:": "🚋",
":trolleybus:": "🚎",
":postal_horn:": "📯",
":factory:": "🏭",
":train2:": "🚆",
":pager:": "📟",
":outbox_tray:": "📤",
":mailbox:": "📫",
":light_rail:": "🚈",
":busstop:": "🚏",
":file_folder:": "📁",
":card_index:": "📇",
":monorail:": "🚝",
":no_bicycles:": "🚳",
":hugging:": "🤗",
":thinking:": "🤔",
":nerd:": "🤓",
":zipper_mouth:": "🤐",
":rolling_eyes:": "🙄",
":upside_down:": "🙃",
":slight_smile:": "🙂",
":writing_hand:": "✍",
":eye:": "👁",
":man_in_suit:": "🕴",
":golfer:": "🏌",
":golfer_woman:": "🏌♀",
":anger_right:": "🗯",
":coffin:": "⚰",
":gear:": "⚙",
":alembic:": "⚗",
":scales:": "⚖",
":keyboard:": "⌨",
":shield:": "🛡",
":bed:": "🛏",
":ballot_box:": "🗳",
":compression:": "🗜",
":wastebasket:": "🗑",
":file_cabinet:": "🗄",
":trackball:": "🖲",
":printer:": "🖨",
":joystick:": "🕹",
":hole:": "🕳",
":candle:": "🕯",
":prayer_beads:": "📿",
":amphora:": "🏺",
":label:": "🏷",
":film_frames:": "🎞",
":level_slider:": "🎚",
":thermometer:": "🌡",
":motorway:": "🛣",
":synagogue:": "🕍",
":mosque:": "🕌",
":kaaba:": "🕋",
":stadium:": "🏟",
":desert:": "🏜",
":cityscape:": "🏙",
":camping:": "🏕",
":rosette:": "🏵",
":volleyball:": "🏐",
":medal:": "🏅",
":popcorn:": "🍿",
":champagne:": "🍾",
":hot_pepper:": "🌶",
":burrito:": "🌯",
":taco:": "🌮",
":hotdog:": "🌭",
":shamrock:": "☘",
":comet:": "☄",
":turkey:": "🦃",
":scorpion:": "🦂",
":lion_face:": "🦁",
":crab:": "🦀",
":spider_web:": "🕸",
":spider:": "🕷",
":chipmunk:": "🐿",
":fog:": "🌫",
":chains:": "⛓",
":pick:": "⛏",
":stopwatch:": "⏱",
":ferry:": "⛴",
":mountain:": "⛰",
":ice_skate:": "⛸",
":skier:": "⛷",
":sad:": "😥",
":egg:": "🥚",
":drum:": "🥁"
};
function convertShortcodes(string) {
if (string.split(":").length > 2) {
for (var i in emojiShortCodes) {
if (string.includes(i)) {
string = string.replaceAll(i, emojiShortCodes[i]);
}
}
}
return string;
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
var sanitizeChat = function (string, maxlength = 500) {
var temp = document.createElement("div");
temp.innerText = string;
temp.innerText = temp.innerHTML;
temp = temp.textContent || temp.innerText || "";
temp = temp.substring(0, Math.min(temp.length, maxlength));
return temp.trim();
};
var sanitizeString = function (str) {
str = str.replace(/[^a-z0-9áéíóúñü \.,_-]/gim, "");
return str.trim();
};
var sanitizeLabel = function (string) {
let temp = document.createElement("div");
temp.innerText = string;
temp.innerText = temp.innerHTML;
temp = temp.textContent || temp.innerText || "";
temp = temp.substring(0, Math.min(temp.length, 100));
return temp.trim();
};
var sanitizeRoomName = function (roomid) {
roomid = roomid.trim();
if (roomid === "") {
return roomid;
} else if (roomid === false) {
return roomid;
}
var sanitized = roomid.replace(/[\W]+/g, "_");
if (roomid.replace(/ /g, "_") !== sanitized) {
if (!session.cleanOutput) {
warnUser("Info: Only AlphaNumeric characters should be used for the room name.\n\nThe offending characters have been replaced by an underscore");
}
}
if (sanitized.length > 30) {
sanitized = sanitized.substring(0, 30);
if (!session.cleanOutput) {
warnUser("The Room name should be less than 31 alPhaNuMeric characters long.\n\nWe will trim it to length.");
}
}
return sanitized;
};
var sanitizePassword = function (passwrd) {
if (passwrd === "") {
return passwrd;
} else if (passwrd === false) {
return passwrd;
} else if (passwrd === null) {
return passwrd;
}
passwrd = passwrd.trim();
if (passwrd.length < 1) {
if (!session.cleanOutput) {
warnUser("The password provided was blank.");
}
}
var sanitized = encodeURIComponent(passwrd); //.replace(/[\W]+/g, "_");
//if (sanitized !== passwrd) {
// if (!(session.cleanOutput)) {
// warnUser("Info: Only AlphaNumeric characters should be used in the password.\n\nThe offending characters have been replaced by an underscore");
// }
//}
return sanitized;
};
function checkConnection() {
if (session.ws === null) {
return;
}
if (!session.cleanOutput) {
if (document.getElementById("qos")) {
// true or false; null might cause problems?
getById("logoname").style.display = "unset";
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
getById("qos").style.color = "#FFF7";
} else {
getById("qos").style.color = "red";
}
}
}
}
session.obsSceneSync = function () {
if (session.layouts && session.obsSceneTriggers && session.obsState && session.obsState.details && session.obsState.details.currentScene.name && session.obsSceneTriggers.includes(session.obsState.details.currentScene.name)) {
var idx = session.obsSceneTriggers.indexOf(session.obsState.details.currentScene.name);
if (idx >= 0) {
if (session.layouts[idx]) {
var layout = combinedLayout(session.layouts[idx]);
if (layout) {
session.layout = layout;
updateMixer();
}
}
}
return true;
}
return false;
};
session.sceneSync = function (UUID) {
if (!session.rpcs[UUID]) {
return;
} else if (!session.rpcs[UUID].videoElement) {
return;
} // i'll want to consider other things, such as canvas at some point.
var msg = {};
msg.sceneDisplay = session.rpcs[UUID].videoElement.style.display != "none";
msg.sceneMute = session.rpcs[UUID].mutedState;
if (session.optimize !== false) {
// if not visible in the scene anymore, lets lets optimize. This is outside the scope of OBS
var bandwidth = parseInt(session.rpcs[UUID].targetBandwidth); // wtf is goign on here?
if (msg.sceneDisplay === false) {
if (bandwidth > session.optimize || bandwidth < 0) {
// limit to optimized bitrate
bandwidth = session.optimize;
}
}
if (session.rpcs[UUID].bandwidth !== bandwidth) {
// bandwidth already set correctly. don't resend.
msg.bitrate = bandwidth;
if (session.sendRequest(msg, UUID)) {
session.rpcs[UUID].bandwidth = bandwidth; // this is letting the system know what the actual bandwidth is, even if it isn't the real target.
} else {
errorlog("Unable to set update OBS Visibility");
}
} else {
session.sendRequest(msg, UUID);
}
} else {
session.sendRequest(msg, UUID);
}
};
var TriggerOnNewDetails = false;
session.obsStateSync = function (data2send = false, uid = false) {
if (session.disableOBS) {
return;
}
if (!window.obsstudio) {
return;
} // this isn't OBS
// they can disable remote control via OBS brower source drop-down itself.
log(data2send);
if (data2send && data2send == "sourceActive" && session.obsState.sourceActive) {
TriggerOnNewDetails = true;
} else if (data2send && data2send == "details" && session.obsState.sourceActive && TriggerOnNewDetails) {
if (session.obsState.details && session.obsState.details.currentScene && session.obsState.details.currentScene.name) {
session.obsState.details.thisScene = session.obsState.details.currentScene.name;
TriggerOnNewDetails = false;
}
}
var needOptimize = false;
if (session.obsState.visibility !== null) {
if (session.obsState.visibility === false) {
/////////////////// I need to change tis to .state or whatever, anc catch/handle these events to update the buttons in the pop up menu
needOptimize = true;
}
}
session.obsSceneSync();
for (var UUID in session.rpcs) {
if (uid && uid !== UUID) {
continue;
} // target just a single connection.
var msg = {};
if (!data2send) {
msg.obsState = session.obsState;
if (session.rpcs[UUID].obsControl === false) {
msg.obsState.details = null; // we don't want to send needless data
}
} else if (data2send in session.obsState) {
if (data2send == "details") {
if (session.rpcs[UUID].obsControl === false) {
continue; // we don't want to send needless data; this isn't a visibility update, so skip.
}
msg.obsState = {};
msg.obsState[data2send] = session.obsState[data2send];
} else {
msg.obsState = {};
msg.obsState[data2send] = session.obsState[data2send];
}
}
if (session.filterOBSscenes && msg.obsState && msg.obsState.details && msg.obsState.details.scenes && msg.obsState.details.scenes.length) {
var scenes = [];
msg.obsState.details.scenes.forEach(scene => {
if (session.filterOBSscenes && session.filterOBSscenes.length) {
if (session.filterOBSscenes.includes(scene)) {
scenes.push(scene);
}
}
});
msg.obsState.details.scenes = scenes;
}
if (session.optimize !== false) {
var bandwidth = parseInt(session.rpcs[UUID].targetBandwidth);
if (needOptimize) {
if (bandwidth > session.optimize || bandwidth < 0) {
// limit to optimized bitrate
bandwidth = session.optimize;
}
}
if (session.rpcs[UUID].bandwidth !== bandwidth) {
// bandwidth already set correctly. don't resend.
msg.bitrate = bandwidth;
warnlog("Message to be sent: ");
warnlog(msg);
if (session.sendRequest(msg, UUID)) {
session.rpcs[UUID].bandwidth = bandwidth; // this is letting the system know what the actual bandwidth is, even if it isn't the real target.
} else {
errorlog("Unable to set update OBS Visibility");
}
} else {
warnlog("Message to be sent: ");
warnlog(msg);
session.sendRequest(msg, UUID);
}
} else {
warnlog("Message to be sent: ");
warnlog(msg);
session.sendRequest(msg, UUID);
}
}
};
session.getOBSOptimization = function (msg, UUID) {
if (session.obsState) {
msg.obsState = {};
var needOptimize = false;
if (session.obsState.visibility !== null) {
msg.obsState.visibility = session.obsState.visibility;
if (session.obsState.visibility === false) {
needOptimize = true;
}
}
if (session.obsState.sourceActive !== null) {
msg.obsState.sourceActive = session.obsState.sourceActive;
//if (session.obsState.sourceActive===false){
// needOptimize=true;
//}
}
if (session.obsState.recording !== null) {
msg.obsState.recording = session.obsState.recording;
}
if (session.obsState.streaming !== null) {
msg.obsState.streaming = session.obsState.streaming;
}
if (session.obsState.virtualcam !== null) {
msg.obsState.virtualcam = session.obsState.virtualcam;
}
}
if (session.optimize !== false) {
msg.optimizedBitrate = parseInt(session.optimize) || 0; // not setting a bitrate; just letting them know what the optimized bitrate is.
if (needOptimize) {
session.rpcs[UUID].bandwidth = msg.optimizedBitrate;
}
}
return msg;
};
function getOBSDetails(callbackname = "details") {
if (session.disableOBS) {
return false;
}
if (!window.obsstudio) {
return;
}
if (!("details" in session.obsState)) {
session.obsState.details = {};
}
var readOnlyFuncs = [
"getControlLevel",
//"getStatus",
"getCurrentScene",
"getScenes"
//"getTransitions",
//"getCurrentTransition",
//"pluginVersion"
];
var promises = {};
promises.main = true;
Object.keys(window.obsstudio).forEach(async key => {
try {
if (typeof window.obsstudio[key] === "function") {
if (readOnlyFuncs.includes(key)) {
try {
promises[key] = true;
window.obsstudio[key](function (out) {
var shortkey = key.replace("get", "");
shortkey = shortkey[0].toLowerCase() + shortkey.slice(1);
session.obsState.details[shortkey] = out;
delete promises[key];
if (!Object.keys(promises).length) {
session.obsStateSync(callbackname);
}
});
} catch (e) {
delete promises[key];
}
}
/* } else if (typeof window.obsstudio[key] === 'object'){ // none of these values I really need right now.
var shortkey = key.replace("get","");
shortkey = shortkey[0].toLowerCase() + shortkey.slice(1);
session.obsState.details[shortkey] = window.obsstudio[key];
} else {
var shortkey = key.replace("get","");
shortkey = shortkey[0].toLowerCase() + shortkey.slice(1);
session.obsState.details[shortkey] = window.obsstudio[key]; */
}
} catch (e) {
errorlog(e);
}
});
delete promises.main;
if (!Object.keys(promises).length) {
session.obsStateSync(callbackname);
}
}
function toggleOBSControls() {
toggle(getById("remoteOBSControl"));
if (getById("remoteOBSControl").style.display == "none") {
getById("modalBackdrop").innerHTML = ""; // Delete modal
getById("modalBackdrop").remove();
} else {
getById("modalBackdrop").innerHTML = ""; // Delete modal
getById("modalBackdrop").remove();
var modalTemplate = ``;
document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end
document.getElementById("modalBackdrop").addEventListener("click", toggleOBSControls);
}
}
function toggleOBSControlsLock(ele, disable = null) {
const element = getById("remoteOBSControlContents");
// If disable is not specified, toggle based on current state
// Otherwise, use the provided value
const shouldDisable = disable !== null ? disable :
element.style.pointerEvents !== 'none';
element.style.pointerEvents = shouldDisable ? 'none' : 'auto';
element.style.opacity = shouldDisable ? '0.6' : '1';
ele.textContent = shouldDisable ? '🔒' : '🔓';
return shouldDisable; // returns the new state
}
function requestOBSAction(ele) {
if (session.disableOBS) {
return false;
}
}
function obsSceneChanged(event) {
log(event.detail.name);
getOBSDetails(); // contains obsStateSync
}
function obsVirtualcamStarted(event) {
session.obsState.virtualcam = true;
session.obsStateSync("virtualcam");
}
function obsVirtualcamStopped(event) {
session.obsState.virtualcam = false;
session.obsStateSync("virtualcam");
}
function obsStreamingStarted(event) {
session.obsState.streaming = true;
session.obsStateSync("streaming");
}
function obsStreamingStopped(event) {
session.obsState.streaming = false;
session.obsStateSync("streaming");
}
function obsRecordingStarted(event) {
session.obsState.recording = true;
session.obsStateSync("recording");
}
function obsRecordingStopped(event) {
session.obsState.recording = false;
session.obsStateSync("recording");
}
function obsSourceActiveChanged(event) {
warnlog("obsSourceActiveChanged");
warnlog(event.detail);
try {
if (typeof event === "boolean") {
var sourceActive = event;
} else if (typeof event.detail === "boolean") {
var sourceActive = event.detail;
} else if (typeof event.detail.active === "boolean") {
var sourceActive = event.detail.active;
} else {
var sourceActive = event.detail.active;
}
if (typeof sourceActive === "undefined") {
return;
} // Just fail.
if (session.obsState.sourceActive !== sourceActive) {
// only move forward if there is a change; the event likes to double fire you see.
session.obsState.sourceActive = sourceActive;
session.obsStateSync("sourceActive");
}
} catch (e) {
errorlog(e);
}
}
function obsSourceVisibleChanged(event) {
// accounts for visible in VDO.Ninja scene AND visible in OBS scene
warnlog("obsSourceVisibleChanged");
warnlog(event.detail);
try {
if (typeof event === "boolean") {
var visibility = event;
} else if (typeof event.detail === "boolean") {
var visibility = event.detail;
} else if (typeof event.detail.visible === "boolean") {
var visibility = event.detail.visible;
} else {
var visibility = event.detail.visible;
}
if (typeof visibility === "undefined") {
// fall back
if (typeof document.visibilityState !== "undefined") {
visibility = document.visibilityState === "visible"; // modern
} else if (typeof document.hidden !== "undefined") {
visibility = !document.hidden; // legacy
} else {
return; // ... unknown input? fail.
}
}
if (session.obsState.visibility !== visibility) {
// only move forward if there is a change; the event likes to double fire you see.
session.obsState.visibility = visibility;
session.obsStateSync("visibility");
}
} catch (e) {
errorlog(e);
}
}
function manageSceneState(data, UUID) {
// incoming obs details
if (session.disableOBS) {
return;
}
var processNeeded = false;
try {
if ("sceneDisplay" in data) {
processNeeded = true;
session.pcs[UUID].sceneDisplay = data.sceneDisplay;
}
if ("sceneMute" in data) {
processNeeded = true;
session.pcs[UUID].sceneMute = data.sceneMute;
}
if (data.obsState) {
if ("sourceActive" in data.obsState) {
processNeeded = true;
session.pcs[UUID].obsState.sourceActive = data.obsState.sourceActive;
}
if ("visibility" in data.obsState) {
processNeeded = true;
session.pcs[UUID].obsState.visibility = data.obsState.visibility;
session.optimizeBitrate(UUID); // &optimize flag; sets video bitrate to target value if this flag == HIDDEN (if optimize=0, disables both audio and video)
}
if ("details" in data.obsState) {
//if (Object.keys(data.obsState.details).length){
processNeeded = true;
session.pcs[UUID].obsState.details = data.obsState.details;
//}
}
if ("streaming" in data.obsState) {
processNeeded = true;
session.pcs[UUID].obsState.streaming = data.obsState.streaming;
}
if ("recording" in data.obsState) {
processNeeded = true;
session.pcs[UUID].obsState.recording = data.obsState.recording;
}
if ("virtualcam" in data.obsState) {
processNeeded = true;
session.pcs[UUID].obsState.virtualcam = data.obsState.virtualcam;
}
}
} catch (e) {
errorlog(e);
}
if (processNeeded) {
log(data);
applySceneState();
} else {
return;
}
if (isIFrame) {
pokeIframeAPI("obs-state", data.obsState, UUID);
}
if (session.obsControls === false) {
return;
}
try {
var control = 0;
if (session.pcs[UUID].obsState && session.pcs[UUID].obsState.details) {
control = parseInt(session.pcs[UUID].obsState.details.controlLevel) || 0; //0 for NONE, 1 for READ_OBS (OBS data), 2 for READ_USER (User data), 3 for BASIC, 4 for ADVANCED and 5 for ALL
}
if (control >= 4) {
if (session.director || !session.roomid) {
if (session.pcs[UUID].remote) {
if (session.obsControls !== false) {
getById("obscontrolbutton").classList.remove("hidden"); // so they get a tip.
}
}
}
}
var multi = false;
getById("obsControlButtons")
.querySelectorAll("[data-system]")
.forEach(ele => {
if (ele.dataset.system in session.pcs) {
if (ele.dataset.system !== UUID) {
multi = true;
}
} else {
// delete, since no longer active.
ele.remove();
}
});
getById("obsSceneNames")
.querySelectorAll("[data-system]")
.forEach(ele => {
if (ele.dataset.system in session.pcs) {
if (ele.dataset.system !== UUID) {
multi = true;
}
} else {
// delete, since no longer active.
ele.remove();
}
});
if (control == 0) {
var obsControlButtonsBox = getById("obsControlButtons").querySelector("[data-system='" + UUID + "']");
if (obsControlButtonsBox) {
obsControlButtonsBox.remove();
}
var obsSceneNamesBox = getById("obsSceneNames").querySelector("[data-system='" + UUID + "']"); // this hides if less than 2, so hide it now.
if (obsSceneNamesBox) {
obsSceneNamesBox.remove();
}
if (!multi) {
getById("obsControlHelp").classList.remove("hidden");
}
return;
}
getById("obsControlHelp").classList.add("hidden");
var obsControlButtonsBox = getById("obsControlButtons").querySelector("[data-system='" + UUID + "']");
if (!obsControlButtonsBox) {
obsControlButtonsBox = document.createElement("div");
obsControlButtonsBox.dataset.system = UUID;
getById("obsControlButtons").appendChild(obsControlButtonsBox);
} else {
obsControlButtonsBox.innerHTML = "";
}
if (multi) {
var h3 = document.createElement("h3");
h3.innerText = "OBS instance: " + (session.pcs[UUID].label || session.pcs[UUID].scene || UUID);
obsControlButtonsBox.appendChild(h3);
}
if (session.pcs[UUID].obsState && "streaming" in session.pcs[UUID].obsState) {
var controlButton = document.createElement("button");
controlButton.dataset.UUID = UUID;
if (session.pcs[UUID].obsState.streaming) {
controlButton.classList.add("pressed");
controlButton.ariaPressed = "true";
controlButton.dataset.obsAction = "stopStreaming";
controlButton.innerText = "📡 stop streaming";
controlButton.classList.remove("hidden");
} else if (session.pcs[UUID].obsState.streaming===false) {
controlButton.classList.remove("hidden");
controlButton.dataset.obsAction = "startStreaming";
controlButton.innerText = "📡 start streaming";
} else {
controlButton.dataset.obsAction = "startStreaming";
controlButton.innerText = "📡 start streaming";
controlButton.classList.remove("hidden");
}
if (control < 5) {
controlButton.disabled = true;
controlButton.style.cursor = "not-allowed";
controlButton.title = "Source is lacking required permissions.";
} else {
controlButton.onclick = async function () {
var msg = {};
msg.obsCommand = {};
msg.obsCommand.action = this.dataset.obsAction;
msg.UUID = this.dataset.UUID;
if (document.querySelector("#obsRemotePassword>input") && document.querySelector("#obsRemotePassword>input").value) {
msg.remote = document.querySelector("#obsRemotePassword>input").value;
} else {
msg.remote = session.remote;
}
msg = await session.encodeRemote(msg);
session.anysend(msg); // this is neat, but doesn't work with websocket. I need to add
log("action request: " + this.dataset.obsAction);
};
}
obsControlButtonsBox.appendChild(controlButton);
}
if (session.pcs[UUID].obsState && "recording" in session.pcs[UUID].obsState) {
var controlButton = document.createElement("button");
controlButton.dataset.UUID = UUID;
if (session.pcs[UUID].obsState.recording) {
controlButton.classList.add("pressed");
controlButton.ariaPressed = "true";
controlButton.dataset.obsAction = "stopRecording";
controlButton.innerText = "📽 stop recording";
controlButton.classList.remove("hidden");
} else if (session.pcs[UUID].obsState.recording===false) {
controlButton.classList.remove("hidden");
controlButton.dataset.obsAction = "startRecording";
controlButton.innerText = "📽 start recording";
} else {
controlButton.classList.remove("hidden");
controlButton.dataset.obsAction = "startRecording";
controlButton.innerText = "📽 start recording";
}
if (control < 5) {
controlButton.disabled = true;
controlButton.style.cursor = "not-allowed";
controlButton.title = "Source is lacking required permissions.";
} else {
controlButton.onclick = async function () {
var msg = {};
msg.obsCommand = {};
msg.obsCommand.action = this.dataset.obsAction;
msg.UUID = this.dataset.UUID;
if (document.querySelector("#obsRemotePassword>input").value) {
msg.remote = document.querySelector("#obsRemotePassword>input").value;
} else {
msg.remote = session.remote;
}
msg = await session.encodeRemote(msg);
session.anysend(msg);
log("action request: " + this.dataset.obsAction);
};
}
obsControlButtonsBox.appendChild(controlButton);
}
if (session.pcs[UUID].obsState && "virtualcam" in session.pcs[UUID].obsState) {
var controlButton = document.createElement("button");
controlButton.dataset.UUID = UUID;
if (session.pcs[UUID].obsState.virtualcam) {
controlButton.classList.add("pressed");
controlButton.ariaPressed = "true";
controlButton.dataset.obsAction = "stopVirtualcam";
controlButton.innerText = "💻 stop virtualcam";
controlButton.classList.remove("hidden");
} else if (session.pcs[UUID].obsState.virtualcam===false) {
controlButton.classList.remove("hidden");
controlButton.dataset.obsAction = "startVirtualcam";
controlButton.innerText = "💻 start virtualcam";
} else {
controlButton.classList.remove("hidden");
controlButton.dataset.obsAction = "startVirtualcam";
controlButton.innerText = "💻 start virtualcam";
}
if (control < 5) {
controlButton.disabled = true;
controlButton.style.cursor = "not-allowed";
controlButton.title = "Source is lacking required permissions.";
} else {
controlButton.onclick = async function () {
var msg = {};
msg.obsCommand = {};
msg.obsCommand.action = this.dataset.obsAction;
msg.UUID = this.dataset.UUID;
if (document.querySelector("#obsRemotePassword>input").value) {
msg.remote = document.querySelector("#obsRemotePassword>input").value;
} else {
msg.remote = session.remote;
}
msg = await session.encodeRemote(msg);
session.anysend(msg);
log("action request: " + this.dataset.obsAction);
};
}
obsControlButtonsBox.appendChild(controlButton);
}
} catch (e) {
errorlog(e);
} // just in case the client has disconnected.
if (control < 2) {
var obsSceneNamesBox = getById("obsSceneNames").querySelector("[data-system='" + UUID + "']");
if (obsSceneNamesBox) {
obsSceneNamesBox.remove();
}
return;
}
var obsSceneNamesBox = getById("obsSceneNames").querySelectorAll("div[data-system='" + UUID + "']");
if (!obsSceneNamesBox.length) {
obsSceneNamesBox = document.createElement("div");
obsSceneNamesBox.dataset.system = UUID;
getById("obsSceneNames").appendChild(obsSceneNamesBox);
} else {
obsSceneNamesBox = obsSceneNamesBox[0];
obsSceneNamesBox.innerHTML = "";
}
if (multi) {
var h3 = document.createElement("h3");
h3.innerText = "OBS instance: " + (session.pcs[UUID].label || session.pcs[UUID].scene || UUID);
obsSceneNamesBox.appendChild(h3);
}
if (session.pcs[UUID].obsState.details) {
var details = session.pcs[UUID].obsState.details;
if (details.scenes) {
details.scenes.forEach(scene => {
var sceneButton = document.createElement("button");
sceneButton.dataset.obsScene = scene;
sceneButton.dataset.UUID = UUID;
sceneButton.innerText = scene;
if (details.currentScene && details.currentScene.name && details.currentScene.name === scene) {
sceneButton.classList.add("pressed");
sceneButton.ariaPressed = "true";
}
obsSceneNamesBox.appendChild(sceneButton);
if (control < 4) {
sceneButton.disabled = true;
sceneButton.style.cursor = "not-allowed";
sceneButton.title = "Source is lacking required permissions.";
} else {
sceneButton.onclick = async function () {
var msg = {};
msg.obsCommand = { action: "setCurrentScene", value: this.dataset.obsScene };
msg.UUID = this.dataset.UUID;
if (document.querySelector("#obsRemotePassword>input").value) {
msg.remote = document.querySelector("#obsRemotePassword>input").value;
} else {
msg.remote = session.remote;
}
msg = await session.encodeRemote(msg);
session.anysend(msg);
log("scene change request: " + this.dataset.obsScene);
};
}
});
}
}
getById("debugRemoteOBSControl").innerText = JSON.stringify(session.pcs[UUID].obsState);
}
function processOBSCommand(msg) {
if (session.disableOBS) {
return false;
} else if (!window.obsstudio) {
return false;
} else if (typeof msg.obsCommand !== "object") {
return false;
} else if ("remote" in msg) {
if ((msg.remote === session.remote && session.remote) || session.remote === true) {
// approved
} else {
if (msg.UUID && msg.obsCommand.action) {
var data = {};
data.rejected = "obsCommand";
//data.debug = msg.remote;
session.sendRequest(data, msg.UUID); // this skips the server
}
warnlog("Denied access; remote does not match");
return false;
}
} else {
if (msg.UUID && msg.obsCommand.action) {
var data = {};
data.rejected = "obsCommand";
//data.debug = "no remote code provided";
session.sendRequest(data, msg.UUID); // this skips the server
}
return false;
}
try {
// {changeScene: this.dataset.obsScene}
if (msg.obsCommand.action && typeof msg.obsCommand.action == "string") {
if (msg.obsCommand.action=="stopVirtualcam" || msg.obsCommand.action=="startVirtualcam"){
if (session.obsState.virtualcam===false){
if (msg.UUID){
var data = {};
data.rejected = msg.obsCommand.action;
session.sendRequest(data, msg.UUID); // this skips the server
}
return false;
}
}
if (msg.obsCommand.action=="stopRecording" || msg.obsCommand.action=="startRecording"){
if (session.obsState.recording===false){
if (msg.UUID){
var data = {};
data.rejected = msg.obsCommand.action;
session.sendRequest(data, msg.UUID); // this skips the server
}
return false;
}
}
if (msg.obsCommand.action=="stopStreaming" || msg.obsCommand.action=="startStreaming"){
if (session.obsState.streaming===false){
if (msg.UUID){
var data = {};
data.rejected = msg.obsCommand.action;
session.sendRequest(data, msg.UUID); // this skips the server
}
return false;
}
}
if (msg.obsCommand.value && typeof msg.obsCommand.value == "string") {
if (msg.obsCommand.action == "setCurrentScene" && session.filterOBSscenes && session.filterOBSscenes.length) {
try {
if (!session.filterOBSscenes.includes(msg.obsCommand.value)) {
return false;
}
} catch (e) {
errorlog(e);
return false;
}
}
window.obsstudio[msg.obsCommand.action](msg.obsCommand.value);
} else {
window.obsstudio[msg.obsCommand.action]();
}
}
} catch (e) {
errorlog(e);
return false;
}
return true;
}
function applySceneState() {
// guest side; tally light, etc.
if (document.getElementById("videosource")) {
var visibility = false;
var ondeck = false;
var recording = false;
if (session.tallyOverride !== false) {
if (session.tallyOverride == 1) {
recording = true;
} else if (session.tallyOverride == 2) {
ondeck = true;
visibility = false;
recording = false;
} else if (session.tallyOverride == 3) {
visibility = true;
recording = false;
} else if (session.tallyOverride == 0) {
ondeck = false;
visibility = false;
recording = false;
} else {
// maybe its a custom message or default?
}
if (!session.cleanOutput) {
getById("obsState").classList.remove("hidden");
}
} else if (!session.disableOBS) {
for (var uid in session.pcs) {
if (session.pcs[uid].obsState.sourceActive !== false && session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) {
visibility = true;
} else if (session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) {
ondeck = true;
}
if ((session.pcs[uid].obsState.recording || session.pcs[uid].obsState.streaming) && session.pcs[uid].obsState.sourceActive !== false && session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) {
// the scene that is recording must be visible also.
recording = true;
}
}
if (!session.cleanOutput) {
getById("obsState").classList.remove("hidden");
}
} else {
return;
}
if (recording) {
getById("obsState").classList.remove("ondeck");
getById("obsState").classList.add("recording"); // TODO: this needs to check all peers to make sure it's valid
getById("obsState").innerHTML = "ON AIR";
if (session.tallyStyle) {
getById("main").classList.remove("ondeck");
getById("main").classList.add("recording");
}
} else if (visibility) {
getById("obsState").classList.remove("recording");
getById("obsState").classList.remove("ondeck");
getById("obsState").innerHTML = "ACTIVE";
if (session.tallyStyle) {
// only show active if its tally is enabled manually
getById("main").classList.remove("recording");
getById("main").classList.remove("ondeck");
} else {
getById("obsState").classList.add("hidden"); // most people don't care about being active
}
} else if (ondeck) {
getById("obsState").classList.remove("recording");
getById("obsState").classList.add("ondeck"); // TODO: this needs to check all peers to make sure it's valid
getById("obsState").innerHTML = "STAND BY";
if (session.tallyStyle) {
getById("main").classList.remove("recording");
getById("main").classList.add("ondeck");
}
} else {
getById("obsState").classList.remove("recording");
getById("obsState").classList.remove("ondeck");
getById("obsState").innerHTML = "INACTIVE";
getById("obsState").classList.add("hidden"); // I don't think most people care to see inactive.
if (session.tallyStyle) {
getById("main").classList.remove("recording");
getById("main").classList.remove("ondeck");
}
}
//miniTranslate(getById("obsState"));
if (visibility) {
// BASIC TALLY LIGHT (on deck disabled)
getById("obsState").classList.add("onair"); // LIVE
if (session.tallyStyle) {
getById("main").classList.add("onair");
}
} else {
getById("obsState").classList.remove("onair");
if (session.tallyStyle) {
getById("main").classList.remove("onair");
}
}
if (session.automute) {
if (!visibility) {
session.micIsolatedAutoMute = [];
if (session.automute !== "2") {
for (var uid in session.pcs) {
if (session.directorList.indexOf(uid) >= 0) {
// allow validated directors to hear the guest
session.micIsolatedAutoMute.push(uid);
}
}
}
} else {
session.micIsolatedAutoMute = false;
}
session.applyIsolatedChat();
}
}
}
function compare_vids(a, b) {
var aa = a.order || 0;
var bb = b.order || 0;
if (aa < bb) {
return 1;
}
if (aa > bb) {
return -1;
}
return 0;
}
function compare_vids_sid(a, b) {
var aa = a.dataset.sid || 0;
var bb = b.dataset.sid || 0;
if (aa > bb) {
return 1;
}
if (aa < bb) {
return -1;
}
return 0;
}
function compare_vids_label(a, b) {
if (a.dataset.UUID && session.rpcs[a.dataset.UUID] && session.rpcs[a.dataset.UUID].label) {
var aa = session.rpcs[a.dataset.UUID].label.toLowerCase();
} else {
var aa = 0;
}
if (b.dataset.UUID && session.rpcs[b.dataset.UUID] && session.rpcs[b.dataset.UUID].label) {
var bb = session.rpcs[b.dataset.UUID].label.toLowerCase();
} else {
var bb = 0;
}
if (aa > bb) {
return 1;
}
if (aa < bb) {
return -1;
}
return 0;
}
function sortByZ(mediaPool, layout) {
function sortABZ(a, b) {
if (layout[a.dataset.sid]) {
var aa = layout[a.dataset.sid].zIndex || layout[a.dataset.sid].z || 0;
} else {
var aa = 0;
}
if (layout[b.dataset.sid]) {
var bb = layout[b.dataset.sid].zIndex || layout[b.dataset.sid].z || 0;
} else {
var bb = 0;
}
if (aa < bb) {
return -1;
}
if (aa > bb) {
return 1;
}
return 0;
}
mediaPool.sort(sortABZ);
return mediaPool;
}
window.onpopstate = function () {
if (session.firstPlayTriggered) {
window.location.reload(true); // deprecated, but it seems to work, so w/e
}
};
var miniPerformerX = null;
var miniPerformerY = null;
function makeMiniDraggableElement(elmnt) {
if (session.disableMouseEvents) {
return;
}
try {
elmnt.dragElement = false;
// elmnt.style.bottom = "auto";
elmnt.style.cursor = "grab";
elmnt.stashonmouseup = null;
elmnt.stashonmousemove = null;
} catch (e) {
errorlog(e);
return;
}
var pos1 = 0;
var pos2 = 0;
var pos3 = 0;
var pos4 = 0;
var timestamp = false;
function elementDrag(e) {
// ON DRAG
timestamp = false;
if (session.infocus) {
return;
}
try {
e = e || window.event;
if (e.type !== "touchmove") {
if ("buttons" in e && e.buttons !== 1) {
closeDragElement(e);
return;
}
e.preventDefault();
}
e.stopPropagation();
elmnt.dragElement = true;
if (e.type === "touchmove") {
pos1 = pos3 - e.touches[0].clientX;
pos2 = pos4 - e.touches[0].clientY;
pos3 = e.touches[0].clientX;
pos4 = e.touches[0].clientY;
} else {
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
}
var topDrag = elmnt.offsetTop - pos2;
if (topDrag > -3 + (window.innerHeight - elmnt.clientHeight)) {
topDrag = -3 + (window.innerHeight - elmnt.clientHeight);
}
miniPerformerY = topDrag;
miniPerformerX = elmnt.offsetLeft - pos1;
if (miniPerformerY > window.innerHeight - elmnt.clientHeight) {
miniPerformerY = window.innerHeight - elmnt.clientHeight;
}
if (miniPerformerX > window.innerWidth - elmnt.clientWidth) {
miniPerformerX = window.innerWidth - elmnt.clientWidth;
}
miniPerformerX = (100 * miniPerformerX) / window.innerWidth;
miniPerformerY = (100 * miniPerformerY) / window.innerHeight;
if (session.widget && !session.leftMiniPreview) {
if (miniPerformerX > 74) {
miniPerformerX = 74;
}
}
if (miniPerformerY < 0) {
miniPerformerY = 0;
} else if (miniPerformerY > 100) {
miniPerformerY = 100;
}
if (miniPerformerX < 0) {
miniPerformerX = 0;
} else if (miniPerformerX > 100) {
miniPerformerX = 100;
}
elmnt.style.right = "unset";
elmnt.style.top = miniPerformerY + "%";
elmnt.style.left = miniPerformerX + "%";
} catch (e) {
errorlog(e);
}
}
function closeDragElement(e) {
// TOUCH END
e = e || window.event;
if (e.type !== "touchend") {
if (e.button !== 0) {
return;
}
document.onmouseup = elmnt.stashonmouseup;
document.onmousemove = elmnt.stashonmousemove;
elmnt.onmouseleave = null;
}
if (session.infocus) {
return;
}
e.preventDefault();
if (timestamp && Date.now() - timestamp > 500) {
// long hold, so this is a drag
e.stopPropagation();
if (e.type === "touchend") {
if (session.infocus === true) {
session.infocus = false;
} else {
session.infocus = true;
log("session: myself");
}
setTimeout(() => updateMixer(), 10);
}
} else if (timestamp && e.type !== "touchend") {
if (session.infocus === true) {
session.infocus = false;
} else {
session.infocus = true;
log("session: myself");
}
setTimeout(() => updateMixer(), 10);
}
}
function dragMouseDown(e) {
////// TOUCH START
if (event.ctrlKey || event.metaKey) {
return;
}
timestamp = Date.now();
e = e || window.event;
if (session.infocus) {
return;
}
e.preventDefault();
if (e.type === "touchstart") {
pos3 = e.touches[0].clientX;
pos4 = e.touches[0].clientY;
elmnt.ontouchend = closeDragElement;
elmnt.ontouchmove = elementDrag;
} else {
if (e.button !== 0) {
return;
}
pos3 = e.clientX;
pos4 = e.clientY;
elmnt.stashonmouseup = document.onmouseup; // I don't want to interfere with other drag events.
elmnt.stashonmousemove = document.onmousemove;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
elmnt.onmouseleave = function (event) {
closeDragElement(event);
};
}
}
elmnt.onmousedown = dragMouseDown;
elmnt.ontouchstart = dragMouseDown;
}
function makeDraggableElement(element) {
if (session.disableMouseEvents) {
return;
} // this is here for a reason. :P
if (!element){
return;
}
element.initialX;
element.initialY;
element.currentX;
element.xOffset = 0;
element.currentY;
element.yOffset = 0;
element.isDragging = false;
element.dragElement = true;
element.addEventListener("mousedown", dragStart);
function dragStart(e) {
element.initialX = e.clientX - element.xOffset;
element.initialY = e.clientY - element.yOffset;
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", dragEnd);
document.addEventListener("onmouseleave", dragEnd);
document.addEventListener("onmouseenter", dragEnd);
element.isDragging = true;
}
function dragEnd(e) {
element.initialX = element.currentX;
element.initialY = element.currentY;
document.removeEventListener("mousemove", drag);
document.removeEventListener("mouseup", dragEnd);
document.removeEventListener("onmouseleave", dragEnd);
document.removeEventListener("onmouseenter", dragEnd);
element.isDragging = false;
}
function drag(e) {
if (element.isDragging) {
element.currentX = e.clientX - element.initialX;
element.currentY = e.clientY - element.initialY;
// Get the dimensions of the viewport
let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
// Get the dimensions of the object
let elementWidth = element.offsetWidth;
let elementHeight = element.offsetHeight;
// console.log('elementWidth:\n',elementWidth)
// console.log('elementHeight:\n',elementHeight)
// Calculate the boundaries
let maxX = vw - elementWidth;
let maxY = vh - elementHeight;
let minX = 0;
let minY = 0;
// Calculate real boundaries (parent position: fixed issues)
let topOffset = 0;
let leftOffset = 0;
let elementOffset = element;
while (elementOffset) {
topOffset += elementOffset.offsetTop;
leftOffset += elementOffset.offsetLeft;
elementOffset = elementOffset.offsetParent;
}
// Adjust the position if it's going beyond the boundaries
let realX = element.currentX + leftOffset;
let realY = element.currentY + topOffset;
if (realX > maxX) {
element.currentX = maxX - leftOffset;
} else if (realX < minX) {
element.currentX = minX - leftOffset;
}
if (realY > maxY) {
element.currentY = maxY - topOffset;
} else if (realY < minY) {
element.currentY = minY - topOffset;
}
// Update the position and offset
element.xOffset = element.currentX;
element.yOffset = element.currentY;
element.style.transform = `translate(${element.currentX}px, ${element.currentY}px)`;
}
}
}
function clearCacheForCurrentSite() {
if ('caches' in window) {
try {
caches.keys().then(function(cacheNames) {
cacheNames.forEach(function(cacheName) {
caches.delete(cacheName);
});
});
log("Cache cleared for current site");
} catch(e){}
} else {
warnlog("Cache API not supported");
}
}
function removeStorage(cname) {
localStorage.removeItem(cname);
}
function clearStorage() {
localStorage.clear();
//clearCacheForCurrentSite(); // cache as well.
if (!session.cleanOutput) {
warnUser("The local storage and saved settings have been cleared", 1000);
}
}
function setStorage(cname, cvalue, hours = 9999) {
// not actually a cookie
var now = new Date();
var item = {
value: cvalue,
expiry: now.getTime() + hours * 60 * 60 * 1000
};
try {
localStorage.setItem(cname, JSON.stringify(item));
} catch (e) {
errorlog(e);
}
}
function getStorage(cname) {
try {
var itemStr = localStorage.getItem(cname);
} catch (e) {
errorlog(e);
return;
}
if (!itemStr) {
return "";
}
var item = JSON.parse(itemStr);
var now = new Date();
if (now.getTime() > item.expiry) {
localStorage.removeItem(cname);
return "";
}
return item.value;
}
function play(streamid = null, UUID = false) {
// play whatever is in the URL params; or filter by a streamID option
log("play stream: " + session.view + " " + streamid);
if (session.viewDirectorOnly) {
if (!(UUID || streamid)) {
warnlog("No UUID and StreamID");
return;
} else if (session.directorList.indexOf(UUID) == -1) {
warnlog("Not a director");
return;
}
}
if (session.view_set) {
var played = false;
for (var j in session.view_set) {
if (streamid === null) {
// play what is in the view list ; not a group room probably
session.watchStream(session.view_set[j]);
played = true;
} else if (streamid === session.view_set[j]) {
// plays if the group room list matches the explicit list
session.watchStream(session.view_set[j]);
played = true;
}
}
if (session.include) {
session.include.forEach(sid => {
if (session.view_set.includes(sid)) {
// already played
} else if (streamid === null) {
// play what is in the view list ; not a group room probably
session.watchStream(sid);
} else if (streamid === sid) {
// plays if the group room list matches the explicit list
session.watchStream(sid);
played = true;
}
});
}
if (!played && streamid) {
if (session.scene !== false) {
if (!session.permaid) {
if (!session.queue) {
// I don't want to deal with queues.
if (session.exclude === false || !session.exclude.includes(streamid)) {
if (UUID) {
if (session.directorList.indexOf(UUID) >= 0) {
warnlog("stream ID added to badStreamList: " + streamid);
session.badStreamList.push(streamid);
// if I uncomment this, the director can mute the solo link.
// session.watchStream(streamid); // changed June 4th 2024. We shouldn't be viewing the stream if on the bad list, no?
}
}
}
}
}
}
}
} else if (streamid && session.exclude !== false) {
if (session.exclude.includes(streamid)) {
// we don't play it at all. (if explicity listed as VIDEO, then OKay.)
} else {
session.watchStream(streamid); // I suppose we do play it.
}
} else if (streamid) {
if (session.optimize === 0){
log("Running special optimize===logic logic for loading rtc connections");
try {
// must be a scene and not an auto scene
if (session.scene && session.activatedStreams.size){
if (session.activatedStreams.has(streamid)){
session.watchStream(streamid);
return;
}
}
if (UUID && streamid) {
if (session.directorList.indexOf(UUID) >= 0) {
session.watchStream(streamid);
}
}
} catch(e){
errorlog(e);
}
} else {
session.watchStream(streamid);
}
} else if (session.include.length) {
session.include.forEach(sid => {
session.watchStream(sid);
});
}
}
function nextQueue() {
if (!session.queue) {
return;
}
if (!session.director) {
return;
}
if (session.queueList.length == 0) {
getById("queuebutton").classList.add("red");
setTimeout(function () {
getById("queuebutton").classList.remove("red");
}, 50);
return;
}
var nextStream = session.queueList.shift();
getById("queuebutton").classList.add("red");
setTimeout(function () {
getById("queuebutton").classList.remove("red");
}, 200);
updateQueue();
session.watchStream(nextStream);
log("next stream loading: " + nextStream);
}
function updateQueue(adding = false) {
if (!session.queue) {
return;
}
if (!session.director) {
return;
}
if (session.queueList.length) {
if (session.queueList.length > 10) {
getById("queueNotification").innerHTML = "‼";
} else {
getById("queueNotification").innerHTML = session.queueList.length;
}
getById("queueNotification").classList.add("queueNotification");
} else {
getById("queueNotification").innerHTML = "";
getById("queueNotification").classList.remove("queueNotification");
}
if (adding) {
if (session.beepToNotify) {
playtone();
showNotification("someone joined the queue", "queue length: " + session.queueList.length);
}
getById("queuebutton").classList.remove("shake");
setTimeout(function () {
getById("queuebutton").classList.add("shake");
}, 10);
}
}
function hideStreamLowBandwidth(bandwidth, UUID) {
if (session.lowBitrateCutoff===false) { // allow 0,but I should probably also do this if there is a disconnect.
return;
}
if (session.directorList.includes(UUID) || session.rpcs[UUID].director) {
if (session.showDirector || session.rpcs[UUID].showDirector) {
// all good
} else {
return; // we don't include the director since not treated as a guest
}
}
if (bandwidth <= session.lowBitrateCutoff) {
//log("bandwidth <= session.lowBitrateCutoff");
log("actual bandwidth: "+ bandwidth+ " < bandwidth cut threshold: " +session.lowBitrateCutoff);
// <= used so 0 can be used as a trigger
if (session.lowBitrateSceneChange) {
changeSceneLowBandwidth(true);
} else if (!session.rpcs[UUID].bandwidthMuted) {
session.rpcs[UUID].bandwidthMuted = true;
updateMixer();
}
} else if (session.lowBitrateSceneChange) {
log("changeSceneLowBandwidth(false)");
changeSceneLowBandwidth(false);
} else if (session.rpcs[UUID].bandwidthMuted) {
session.rpcs[UUID].bandwidthMuted = false;
if (session.rpcs[UUID].videoElement) {
session.rpcs[UUID].videoElement.muted = checkMuteState(UUID);
}
updateMixer();
}
}
var changeSceneEnabled = false;
var changeSceneLowBandwidthRevert = false;
function changeSceneLowBandwidth(state) {
if (!session.lowBitrateSceneChange) {
return;
}
if (!session.obsState) {
return;
}
try {
if (session.obsState.sourceActive && session.obsState.details && session.obsState.details.currentScene) {
changeSceneLowBandwidthRevert = session.obsState.details.currentScene.name || false;
} else if ("sourceActive" in session.obsState && !session.obsState.sourceActive && session.obsState.details && session.obsState.details.currentScene) {
if (session.obsState.details.currentScene.name !== session.lowBitrateSceneChange) {
return; // not the FML scene, nor are we visible, so we're not going to switch back. Assume the user has overtaken the setup.
}
}
if (!window.obsstudio || !window.obsstudio["setCurrentScene"]) {
return;
}
if (state && changeSceneLowBandwidthRevert) {
if (changeSceneEnabled) {
// bitrate was higher , so we can now cut off.
//log("2 changeSceneEnabled: "+session.lowBitrateSceneChange+" changeSceneEnabled:"+changeSceneEnabled);
log("Changing to cut scene due to low bandwidth");
window.obsstudio["setCurrentScene"](session.lowBitrateSceneChange);
} else {
log("Low bandwidth, but not changing scenes because threshold not hit at least once yet");
}
} else if (changeSceneLowBandwidthRevert) {
changeSceneEnabled = true;
//log("1 setCurrentScene: "+changeSceneLowBandwidthRevert+" changeSceneEnabled:"+changeSceneEnabled);
log("Reverting to original scene due to good bandwidth");
window.obsstudio["setCurrentScene"](changeSceneLowBandwidthRevert);
}
} catch (e) {
errorlog(e);
}
}
function setupIncomingScreenTracking(v, UUID) {
// SCREEN element.
if (session.directorList.indexOf(UUID) >= 0) {
v.muted = false;
}
v.addEventListener(
"playing",
e => {
try {
var bigPlayButton = document.getElementById("bigPlayButton");
if (bigPlayButton) {
bigPlayButton.parentNode.removeChild(bigPlayButton);
}
} catch (e) {}
resetupAudioOut(e.target, true);
try {
if (session.pip) {
if (v.readyState >= 3) {
if (!v.pip) {
v.pip = true;
toggleSystemPip(v, true);
}
}
}
} catch (e) {}
},
{ once: true }
);
v.onpause = event => {
// prevent things from pausing; human or other
if (v.dataset.UUID && session.rpcs[v.dataset.UUID] && session.rpcs[v.dataset.UUID].manualBandwidth === 0) {
return true;
}
if (!(event.ctrlKey || event.metaKey)) {
warnlog("Video paused; force it to play again");
//return;
//session.audioCtx.resume();
//log("ctx resume");
event.currentTarget
.play()
.then(_ => {
log("playing 4");
})
.catch(error => {
warnlog("didnt play 1");
});
if (Firefox) {
unPauseVideo(v);
}
}
return true;
};
if (session.pip) {
v.onloadedmetadata = function () {
if (!v.paused) {
if (!v.pip) {
v.pip = true;
toggleSystemPip(v, true);
}
}
};
}
v.addEventListener("resize", e => {
// if the aspect ratio changes, then we might want to update the mixer. If audio only, then this doesn't matter.
var v = e.target;
var aspectRatio = parseFloat(v.videoWidth / v.videoHeight) || 0;
log("resize event: " + aspectRatio);
if (!aspectRatio) {
v.resetAR = true;
return;
} // if Audio only, then we don't want to set or update any aspect ratio.
if (v.resetAR) {
log("ASPECT RATIO UNMUTED");
delete v.resetAR;
v.dataset.aspectRatio = aspectRatio;
pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
setTimeout(function () {
updateMixer();
}, 1);
} else if (v.dataset.aspectRatio) {
if (aspectRatio != parseFloat(v.dataset.aspectRatio)) {
log("ASPECT RATIO CHANGED");
v.dataset.aspectRatio = aspectRatio;
pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
setTimeout(function () {
updateMixer();
}, 1); // We don't want to run this on the first resize? just subsequent ones.
}
} else {
log("NEW VIDEO ? ASPECT RATIO new");
v.dataset.aspectRatio = aspectRatio;
pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
setTimeout(function () {
updateMixer();
}, 1);
}
});
if (typeof session.volume == "number") {
v.volume = session.volume;
} else {
v.volume = 1.0; // play audio automatically
}
v.autoplay = true;
v.controls = session.showControls || false;
v.classList.add("tile");
v.setAttribute("playsinline", "");
v.controlTimer = null;
v.dataset.menu = "context-menu-video";
if (!session.cleanOutput) {
v.classList.add("task"); // this adds the right-click menu
}
if (document.getElementById("mainmenu")) {
var m = getById("mainmenu");
m.remove();
document.querySelectorAll(".hidden2").forEach(ele2 => {
ele2.classList.remove("hidden2");
});
}
if (session.director) {
if (session.showControls !== null) {
v.controls = session.showControls;
} else {
v.controls = true;
}
var container = getById("screenContainer_" + UUID);
v.container = container;
v.disablePictureInPicture = false;
v.setAttribute("controls", "controls");
container.appendChild(v);
pokeIframeAPI("control-box-video-updated", v.id, UUID);
session.requestRateLimit(session.directorViewBitrate, UUID); /// limit resolution for director
v.title = "Hold CTRL or CMD (⌘) while clicking the video to open detailed stats";
if (session.beepToNotify) {
playtone();
}
} else if (session.scene !== false) {
v.controls = session.showControls || false;
if (session.view) {
// specific video to be played
v.style.display = "block";
} else if (session.scene === "0") {
// auto plays, right?
v.style.display = "block";
} else {
// group scene I guess; needs to be added manually
v.style.display = "none";
v.mutedStateScene = true;
}
setTimeout(function () {
updateMixer();
}, 1);
} else if (session.roomid !== false) {
if (session.cleanOutput) {
v.controls = session.showControls || false;
} else if (session.studioSoftware) {
v.controls = session.showControls || false;
} else if (session.showControls !== null) {
v.controls = session.showControls;
} else {
v.controls = true;
}
//if ((session.roomid==="") && (session.bitrate)){
// let's keep the default bitrates, since this isn't a real room and bitrates are specified.
//} //else if (session.novideo !== false){
// if (session.novideo.includes(session.rpcs[UUID].streamID)){ // no video will have muted the video already anyways.
// session.requestRateLimit(0,UUID, false);// optimizing audio here doesn't later get turned back on. let the automixer disable audio instead
// }
//} //else {
// session.requestRateLimit(0,UUID, false);//// optimizing audio here doesn't later get turned back on. let the automixer disable audio instead
//}
setTimeout(function () {
updateMixer();
}, 1);
} else {
v.style.display = "block";
setTimeout(function () {
updateMixer();
}, 1);
}
v.addEventListener("click", function (e) {
// show stats of video if double clicked
log("clicked");
try {
var uid = e.currentTarget.dataset.UUID;
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
if (session.statsMenu !== false) {
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
}
e.stopPropagation();
return false;
} else if ("prePausedBandwidth" in session.rpcs[uid]) {
unPauseVideo(e.currentTarget);
}
} catch (e) {
errorlog(e);
}
});
if (session.statsMenu) {
if ("stats" in session.rpcs[UUID]) {
if (getById("menuStatsBox")) {
clearInterval(getById("menuStatsBox").interval);
getById("menuStatsBox").remove();
}
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, UUID);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID);
}
}
v.touchTimeOut = null;
v.touchLastTap = 0;
v.touchCount = 0;
v.addEventListener("touchend", function (event) {
if (session.disableMouseEvents) {
return;
}
log("touched");
//document.ontouchup = null;
//document.onmouseup = null;
document.onmousemove = null;
document.ontouchmove = null;
var currentTime = new Date().getTime();
var tapLength = currentTime - v.touchLastTap;
clearTimeout(v.touchTimeOut);
if (tapLength < 500 && tapLength > 0) {
///
log("double touched");
v.touchCount += 1;
event.preventDefault();
if (v.touchCount < 5) {
v.touchLastTap = currentTime;
return false;
}
v.touchLastTap = 0;
v.touchCount = 0;
log("double touched");
if (session.statsMenu !== false) {
var uid = event.currentTarget.dataset.UUID;
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
}
event.stopPropagation();
return false;
//////
} else {
v.touchCount = 1;
v.touchTimeOut = setTimeout(
function (vv) {
clearTimeout(vv.touchTimeOut);
vv.touchLastTap = 0;
vv.touchCount = 0;
},
5000,
v
);
v.touchLastTap = currentTime;
}
});
if (v.controls == false) {
v.addEventListener("click", function () {
if (v.paused) {
log("PLAYING MANUALLY?");
v.play()
.then(_ => {
log("playing 5");
})
.catch(warnlog);
}
});
if (session.nocursor == false) {
// we do not want to show the controls. This is because MacOS + OBS does not work; so electron app needs this.
if (!session.cleanOutput) {
if (session.studioSoftware) {
} else if (session.showControls === false) {
// explicitly disabled; default null.
} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
} else {
if (v.controlTimer) {
clearInterval(v.controlTimer);
}
v.controlTimer = setTimeout(showControlBar.bind(null, v), 1000);
//v.controlTimer = setTimeout(function (){v.controls=true;},3000); // 3 seconds before I enable the controls automatically. This way it doesn't auto appear during loading. 3s enough, right?
}
}
}
}
//if (session.fadein){
v.addEventListener("animationend", function (e) {
v.classList.remove("fadein"); // allows the video to fade in.
if (v.holder) {
v.holder.classList.remove("fadein");
}
});
// v.classList.add("fadein"); // allows the video to fade in.
// if (v.holder){
// v.holder.classList.add("fadein");
// }
//}
applyMuteState(UUID); // TODO; needs to be specific to screen video
v.usermuted = false;
v.addEventListener("volumechange", function (e) {
var muteState = checkMuteState(UUID);
if (this.muted && this.muted !== muteState) {
this.usermuted = 1;
} else if (!this.muted && this.muted !== muteState) {
this.usermuted = 2;
} else if (!this.muted) {
this.usermuted = false;
}
});
if (session.screenShareStartPaused) {
// we know this is a screen share already
pauseVideo(v, false);
}
if (session.director) {
/* var wss = "";
if (session.customWSS || session.wssSetViaUrl){
if (session.customWSS && (session.customWSS!==true)){
wss = "&pie="+session.customWSS;
} else if (session.customWSS==true){
wss = "&wss=" + session.wss;
} else {
wss = "&wss2=" + session.wss;
}
} */
/*
var codecGroupFlag="";
if (session.codecGroupFlag){
codecGroupFlag = session.codecGroupFlag;
} */
/* var passAdd2="";
if (session.password){
if (session.defaultPassword===false){
passAdd2="&password="+session.password;
}
} */
if (session.customWSS && "isScene" in msg && msg.isScene !== false) {
// this is a scene, so lets not show it.
} else {
var soloLink = soloLinkGenerator(session.rpcs[UUID].streamID);
createControlBoxScreenshare(UUID, soloLink, session.rpcs[UUID].streamID);
}
}
if (session.autorecord || session.autorecordremote) {
log("AUTO RECORD START");
setTimeout(
function (UUID, v) {
var videoKbps = session.recordDefault;
if (session.recordLocal !== false) {
videoKbps = session.recordLocal;
}
if (session.director) {
recordVideo(document.querySelector("[data-action-type='recorder-local'][data--u-u-i-d='" + UUID + "']"), null, videoKbps);
} else if (v.stopWriter || v.recording) {
} else if (v.startWriter) {
v.startWriter();
} else {
recordLocalVideo(null, videoKbps, v);
}
},
2000,
UUID,
v
);
}
if (session.rpcs[UUID]) {
clearTimeout(session.rpcs[UUID].getStatsTimeout);
session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, 100, UUID);
}
}
function setupIncomingVideoTracking(v, UUID) {
// video element.
if (session.directorList.indexOf(UUID) >= 0) {
v.muted = false;
}
v.onpause = event => {
// prevent things from pausing; human or other
if (v.dataset.UUID && session.rpcs[v.dataset.UUID] && session.rpcs[v.dataset.UUID].manualBandwidth === 0) {
return true;
}
if (!CtrlPressed) {
warnlog("Video paused; force it to play again");
//return;
//session.audioCtx.resume();
//log("ctx resume");
event.currentTarget
.play()
.then(_ => {
log("playing 6");
})
.catch(error => {
warnlog("didnt play 1");
});
unPauseVideo(v);
} else if (Firefox && CtrlPressed) {
log("CLICK 351");
if (session.statsMenu !== false) {
var uid = event.currentTarget.dataset.UUID;
event.preventDefault();
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
}
event.stopPropagation();
return false;
}
return true;
};
/* v.onerror = function(event){
errorlog(event);
try{
warnlog("Vidieo element threw an error; going to reconnect it");
session.rpcs[UUID].videoElement.stop();
session.rpcs[UUID].videoElement.srcObject = null;
session.rpcs[UUID].videoElement.srcObject = session.rpcs[UUID].streamSrc; // replaecd with updateIncomingVideoElement these days
session.rpcs[UUID].videoElement.play();
setTimeout(function(){updateMixer();},1);
} catch(e){errorlog(e);}
} */
if (session.pip) {
v.onloadedmetadata = function () {
if (!v.paused) {
if (!v.pip) {
v.pip = true;
toggleSystemPip(v, true);
}
}
};
}
v.addEventListener("resize", e => {
var v = e.target;
var aspectRatio = parseFloat(v.videoWidth / v.videoHeight) || 0;
log("resize event: " + aspectRatio);
if (!aspectRatio) {
v.resetAR = true;
return;
} // if Audio only, then we don't want to set or update any aspect ratio.
if (typeof v.manualRotate == "number"){
//v.rotated = v.manualRotate; // ((session.rotate || 0) + 90) % 360;
} else if (session.keepIncomingVideosInLandscape) {
if (aspectRatio < 1) {
// session.keepIncomingVideosInLandscape
v.rotated = session.keepIncomingVideosInLandscape;
} else {
v.rotated = 0;
}
} else if (session.keepIncomingVideosInPortrait) {
if (aspectRatio > 1) {
// session.keepIncomingVideosInLandscape
v.rotated = session.keepIncomingVideosInPortrait;
} else {
v.rotated = 0;
}
}
if (v.resetAR) {
log("ASPECT RATIO UNMUTED");
delete v.resetAR;
v.dataset.aspectRatio = aspectRatio;
pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
setTimeout(function () {
updateMixer();
}, 1);
} else if (v.dataset.aspectRatio) {
if (aspectRatio != parseFloat(v.dataset.aspectRatio)) {
log("ASPECT RATIO CHANGED");
v.dataset.aspectRatio = aspectRatio;
pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
setTimeout(function () {
updateMixer();
}, 1); // We don't want to run this on the first resize? just subsequent ones.
}
} else {
log("NEW VIDEO ? ASPECT RATIO new");
v.dataset.aspectRatio = aspectRatio;
pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid);
setTimeout(function () {
updateMixer();
}, 1);
}
});
if (typeof session.volume == "number") {
v.volume = session.volume;
} else {
v.volume = 1.0; // play audio automatically
}
v.autoplay = true;
v.controls = session.showControls || false;
v.classList.add("tile");
v.setAttribute("playsinline", "");
v.controlTimer = null;
v.dataset.menu = "context-menu-video";
if (!session.cleanOutput) {
v.classList.add("task"); // this adds the right-click menu
}
if (document.getElementById("mainmenu")) {
var m = getById("mainmenu");
m.remove();
document.querySelectorAll(".hidden2").forEach(ele2 => {
ele2.classList.remove("hidden2");
});
}
if (session.director) {
if (session.showControls === false) {
v.controls = false;
} else {
v.controls = true;
}
var container = getById("videoContainer_" + UUID);
v.container = container;
v.disablePictureInPicture = false;
v.setAttribute("controls", "controls");
container.appendChild(v);
pokeIframeAPI("control-box-video-updated", v.id, UUID);
container.classList.add("hasMedia");
session.requestRateLimit(session.directorViewBitrate, UUID); /// limit resolution for director
v.title = "Hold CTRL or CMD (⌘) while clicking the video to open detailed stats";
if (session.beepToNotify) {
playtone();
}
} else if (session.scene !== false) {
v.controls = session.showControls || false;
if (session.view) {
// specific video to be played
v.style.display = "block";
} else if (session.scene === "0") {
// auto plays, right?
v.style.display = "block";
} else if (session.scene !== false && session.autoadd && session.rpcs[UUID].streamID && session.autoadd.includes(session.rpcs[UUID].streamID)) {
/// session.autoadd
v.style.display = "block"; // auto added because manually added.
} else {
// group scene I guess; needs to be added manually
v.style.display = "none";
session.rpcs[UUID].mutedStateScene = true;
}
} else if (session.roomid !== false) {
if (session.cleanOutput) {
v.controls = session.showControls || false;
} else if (session.studioSoftware) {
v.controls = session.showControls || false;
} else if (session.showControls !== null) {
v.controls = session.showControls;
} else {
v.controls = true;
}
} else {
v.style.display = "block";
}
v.addEventListener("click", function (e) {
// show stats of video if double clicked
log("clicked");
try {
var uid = e.currentTarget.dataset.UUID;
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
if (session.statsMenu !== false) {
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
}
e.stopPropagation();
return false;
} else if ("prePausedBandwidth" in session.rpcs[uid]) {
unPauseVideo(e.currentTarget);
}
} catch (e) {
errorlog(e);
}
});
if (session.statsMenu) {
if ("stats" in session.rpcs[UUID]) {
if (getById("menuStatsBox")) {
clearInterval(getById("menuStatsBox").interval);
getById("menuStatsBox").remove();
}
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, UUID);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID);
}
}
v.touchTimeOut = null;
v.touchLastTap = 0;
v.touchCount = 0;
v.addEventListener("touchend", function (event) {
if (session.disableMouseEvents) {
return;
}
log("touched");
//document.ontouchup = null;
//document.onmouseup = null;
document.onmousemove = null;
document.ontouchmove = null;
var currentTime = new Date().getTime();
var tapLength = currentTime - v.touchLastTap;
clearTimeout(v.touchTimeOut);
if (tapLength < 500 && tapLength > 0) {
///
log("double touched");
v.touchCount += 1;
event.preventDefault();
if (v.touchCount < 5) {
v.touchLastTap = currentTime;
return false;
}
v.touchLastTap = 0;
v.touchCount = 0;
log("double touched");
if (session.statsMenu !== false) {
var uid = event.currentTarget.dataset.UUID;
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
}
event.stopPropagation();
return false;
//////
} else {
v.touchCount = 1;
v.touchTimeOut = setTimeout(
function (vv) {
clearTimeout(vv.touchTimeOut);
vv.touchLastTap = 0;
vv.touchCount = 0;
},
5000,
v
);
v.touchLastTap = currentTime;
}
});
if (session.rpcs[UUID].stats.info && "remote" in session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote) {
v.addEventListener("wheel", remotePTZRequest);
// v.addEventListener("wheel", remoteFocusZoomRequest); // just remote focus -- obsolete.
}
if (session.ptzSlider && (session.director || (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote))) {
const ptzContainer = document.createElement('div');
ptzContainer.className = 'video-ptz-controls';
// Zoom slider
const zoomSlider = document.createElement('div');
zoomSlider.className = 'video-zoom-slider';
const zoomLabel = document.createElement('label');
zoomLabel.innerText = "Zoom";
const zoomInput = document.createElement('input');
zoomInput.title = "Camera zoom control";
zoomInput.type = 'range';
zoomInput.min = '0';
zoomInput.max = '100';
zoomInput.value = '0';
let zoomUpdating = false;
zoomInput.addEventListener('input', (e) => {
if (zoomUpdating) return;
const zoomValue = parseInt(e.target.value) / 100; // Normalize to 0-1
session.requestZoomChange(zoomValue, UUID, session.remote, true);
});
// Pan slider
const panSlider = document.createElement('div');
panSlider.className = 'video-pan-slider';
const panLabel = document.createElement('label');
panLabel.innerText = "Pan";
const panInput = document.createElement('input');
panInput.title = "Camera pan control (left/right)";
panInput.type = 'range';
panInput.min = '-100';
panInput.max = '100';
panInput.value = '0';
let panUpdating = false;
panInput.addEventListener('input', (e) => {
if (panUpdating) return;
const panValue = parseInt(e.target.value) / 100; // Normalize to -1 to 1
session.requestPanChange(panValue, UUID, session.remote, true);
});
// Tilt slider
const tiltSlider = document.createElement('div');
tiltSlider.className = 'video-tilt-slider';
const tiltLabel = document.createElement('label');
tiltLabel.innerText = "Tilt";
const tiltInput = document.createElement('input');
tiltInput.title = "Camera tilt control (up/down)";
tiltInput.type = 'range';
tiltInput.min = '-100';
tiltInput.max = '100';
tiltInput.value = '0';
let tiltUpdating = false;
tiltInput.addEventListener('input', (e) => {
if (tiltUpdating) return;
const tiltValue = parseInt(e.target.value) / 100; // Normalize to -1 to 1
session.requestTiltChange(tiltValue, UUID, session.remote, true);
});
// Append elements to containers
zoomSlider.appendChild(zoomLabel);
zoomSlider.appendChild(zoomInput);
panSlider.appendChild(panLabel);
panSlider.appendChild(panInput);
tiltSlider.appendChild(tiltLabel);
tiltSlider.appendChild(tiltInput);
ptzContainer.appendChild(zoomSlider);
ptzContainer.appendChild(panSlider);
ptzContainer.appendChild(tiltSlider);
if (!v.container) {
v.container = getById("videoContainer_" + UUID);
}
v.container.appendChild(ptzContainer);
// Store references for external updates
session.rpcs[UUID].zoomSlider = (value) => {
zoomUpdating = true;
zoomInput.value = Math.round(value * 100); // Convert 0-1 to 0-100
zoomUpdating = false;
};
session.rpcs[UUID].panSlider = (value) => {
panUpdating = true;
panInput.value = Math.round(value * 100); // Convert -1 to 1 to -100 to 100
panUpdating = false;
};
session.rpcs[UUID].tiltSlider = (value) => {
tiltUpdating = true;
tiltInput.value = Math.round(value * 100); // Convert -1 to 1 to -100 to 100
tiltUpdating = false;
};
} else if (session.zoomSlider && (session.director || (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote))){
const slider = document.createElement('div');
slider.className = 'video-zoom-slider0';
const input = document.createElement('input');
input.title = "Hint: The remote camera's browser may needs to be visible for zoom to work in certain browsers";
input.type = 'range';
input.min = '0';
input.max = '255';
input.value = '0';
let updating = false;
input.addEventListener('input', (e) => {
if (updating) return;
const zoomValue = parseInt(e.target.value) / 255;
session.requestZoomChange(zoomValue, UUID, session.remote, true);
});
// Update slider if remote changes occur
const updateSlider = (value) => {
updating = true;
input.value = Math.round(value * 255);
updating = false;
input.title = input.value*100 + "";
};
slider.appendChild(input);
if (!v.container){
v.container = getById("videoContainer_" + UUID);
}
v.container.appendChild(slider);
// Store reference for external updates
session.rpcs[UUID].zoomSlider = updateSlider;
}
if (v.controls == false) {
v.addEventListener("click", function () {
log("click 33");
if (v.paused) {
log("PLAYING MANUALLY?");
v.play()
.then(_ => {
log("playing 7");
})
.catch(warnlog);
}
});
if (session.nocursor == false) {
// we do not want to show the controls. This is because MacOS + OBS does not work; so electron app needs this.
if (!session.cleanOutput) {
if (session.studioSoftware) {
} else if (session.showControls === false) {
// explicitly disabled; default null.
} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
} else {
if (v.controlTimer) {
clearInterval(v.controlTimer);
}
v.controlTimer = setTimeout(showControlBar.bind(null, v), 1000);
//v.controlTimer = setTimeout(function (){v.controls=true;},3000); // 3 seconds before I enable the controls automatically. This way it doesn't auto appear during loading. 3s enough, right?
}
}
}
}
//if (session.fadein){
v.addEventListener("animationend", function (e) {
v.classList.remove("fadein"); // allows the video to fade in.
if (v.holder) {
v.holder.classList.remove("fadein");
}
});
//v.classList.add("fadein"); // allows the video to fade in.
// if (v.holder){
//// v.holder.classList.add("fadein");
// }
//}
applyMuteState(UUID);
v.usermuted = false;
if (session.screenShareStartPaused && session.rpcs[UUID].screenShareState) {
pauseVideo(v, false);
}
v.addEventListener("volumechange", function (e) {
var muteState = checkMuteState(UUID);
if (this.muted && this.muted !== muteState) {
this.usermuted = 1;
} else if (!this.muted && this.muted !== muteState) {
this.usermuted = 2;
} else if (!this.muted) {
this.usermuted = false;
}
});
if (session.autorecord || session.autorecordremote) {
log("AUTO RECORD START");
setTimeout(
function (UUID, v) {
var videoKbps = session.recordDefault;
if (session.recordLocal !== false) {
videoKbps = session.recordLocal;
}
if (session.director) {
recordVideo(document.querySelector("[data-action-type='recorder-local'][data--u-u-i-d='" + UUID + "']"), null, videoKbps);
} else if (v.stopWriter || v.recording) {
} else if (v.startWriter) {
v.startWriter();
} else {
recordLocalVideo(null, videoKbps, v);
}
},
2000,
UUID,
v
);
}
if (session.rpcs[UUID]) {
clearTimeout(session.rpcs[UUID].getStatsTimeout);
session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, 100, UUID);
}
}
session.requestPanChange = async function(pan, UUID, passwd = session.remote, absolute=false) {
// pan is now expected to be a value between -1 and 1
log("request pan change: " + pan);
var msg = {};
msg.pan = pan; // Normalized value -1 to 1
msg.remote = passwd;
msg.abs = absolute;
msg = await session.encodeRemote(msg);
if (session.sendRequest(msg, UUID)) {
log("pan success");
return true;
} else {
errorlog("failed to send pan change request");
return false;
}
};
session.requestTiltChange = async function(tilt, UUID, passwd = session.remote, absolute=false) {
// tilt is now expected to be a value between -1 and 1
log("request tilt change: " + tilt);
var msg = {};
msg.tilt = tilt; // Normalized value -1 to 1
msg.remote = passwd;
msg.abs = absolute;
msg = await session.encodeRemote(msg);
if (session.sendRequest(msg, UUID)) {
log("tilt success");
return true;
} else {
errorlog("failed to send tilt change request");
return false;
}
};
session.requestZoomChange = async function(zoom, UUID, passwd = session.remote, absolute=false) {
// zoom is now expected to be a value between 0 and 1
log("request zoom change: " + zoom);
var msg = {};
msg.zoom = zoom; // Normalized value 0 to 1
msg.abs = absolute;
msg.remote = passwd;
msg = await session.encodeRemote(msg);
if (session.sendRequest(msg, UUID)) {
log("zoom success");
return true;
} else {
errorlog("failed to send zoom change request");
return false;
}
};
session.requestFocusChange = async function (focal, UUID, passwd = session.remote, absolute=false) {
log("request focus change: " + focal);
var msg = {};
msg.focus = focal;
msg.abs = absolute;
msg.remote = passwd;
msg = await session.encodeRemote(msg);
if (session.sendRequest(msg, UUID)) {
log("focus success");
} else {
errorlog("failed to send focus change request");
}
};
function remotePTZRequest(event) {
event.preventDefault();
var scale = event.deltaY > 0 ? -0.05 : 0.05; // Use larger normalized steps
if (!event.altKey) {
scale *= 2; // Double the scale when not holding Alt
}
if (event.ctrlKey || event.metaKey) {
if (event.shiftKey) {
// tilt: -1 to 1
session.requestTiltChange(scale, event.currentTarget.dataset.UUID);
} else {
// focus: -1 to 1
session.requestFocusChange(scale, event.currentTarget.dataset.UUID);
}
} else if (event.shiftKey) {
// pan: -1 to 1
session.requestPanChange(scale, event.currentTarget.dataset.UUID);
} else {
// zoom: 0 to 1 (relative)
session.requestZoomChange(scale, event.currentTarget.dataset.UUID);
}
}
function remoteFocusZoomRequest(event) { // obsolete.
event.preventDefault();
var scale = event.deltaY> 0 ? -0.004 : 0.004;
log(event.currentTarget);
log(event.deltaY);
if (!event.altKey){
scale*=10;
}
if (event.ctrlKey || event.metaKey) {
// focus
session.requestFocusChange(scale, event.currentTarget.dataset.UUID);
} else {
// zoom
session.requestZoomChange(scale, event.currentTarget.dataset.UUID);
}
}
function mediaAudioTrackUpdated(UUID, streamID) {
pokeIframeAPI("new-audio-track-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess.
}
function mediaVideoTrackUpdated(UUID, streamID) {
pokeIframeAPI("new-video-track-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess.
}
function mediaSourceUpdated(UUID, streamID) {
pokeIframeAPI("new-stream-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess.
pokeAPI("streamAdded", streamID);
}
function showControlBar(vel) {
try {
vel.controls = true;
} catch (e) {
errorlog(e);
}
}
function createRichVideoElement(UUID) {
// this function is used to check and generate a rich video element if needed
if (!session.rpcs[UUID].videoElement) {
log("video element is being created and any media tracks added");
session.rpcs[UUID].videoElement = createVideoElement();
session.rpcs[UUID].videoElement.dataset.UUID = UUID;
session.rpcs[UUID].videoElement.id = "videosource_" + UUID; // could be set to UUID in the future
if (session.rpcs[UUID].streamID) {
session.rpcs[UUID].videoElement.dataset.sid = session.rpcs[UUID].streamID;
}
if (session.rpcs[UUID].rotate !== false) {
session.rpcs[UUID].videoElement.rotated = session.rpcs[UUID].rotate;
session.rpcs[UUID].videoElement.dataset.rotated = session.rpcs[UUID].rotate;
}
session.rpcs[UUID].videoElement.addEventListener(
"playing",
e => {
try {
var bigPlayButton = document.getElementById("bigPlayButton");
if (bigPlayButton) {
bigPlayButton.parentNode.removeChild(bigPlayButton);
}
} catch (e) {}
resetupAudioOut(e.target, true);
try {
if (session.pip) {
if (v.readyState >= 3) {
if (!v.pip) {
v.pip = true;
toggleSystemPip(v, true);
}
}
}
} catch (e) {}
},
{ once: true }
);
if (session.rpcs[UUID].mirrorState) {
applyMirrorGuest(session.rpcs[UUID].mirrorState, session.rpcs[UUID].videoElement);
} else if (session.rpcs[UUID].mirrorState === false) {
applyMirrorGuest(session.rpcs[UUID].mirrorState, session.rpcs[UUID].videoElement);
}
if (session.posterImage) {
session.rpcs[UUID].videoElement.poster = session.posterImage;
}
setupIncomingVideoTracking(session.rpcs[UUID].videoElement, UUID);
pokeIframeAPI("video-element-created", "videosource_" + UUID, UUID);
}
return session.rpcs[UUID].videoElement;
}
function updateVolume(update = false) {
if (session.audioGain !== false) {
if (update) {
if (session.roomid) {
var pswd = session.password || "";
generateHash(session.streamID + session.roomid + pswd + session.salt, 6).then(function (hash) {
setStorage("micVolume_" + hash, session.audioGain, (hours = 6));
});
}
}
if (session.audioGain === 0) {
getById("header").classList.add("orange");
getById("head7").classList.remove("hidden");
} else {
getById("header").classList.remove("orange");
getById("head7").classList.add("hidden");
}
} else {
var pswd = session.password || "";
generateHash(session.streamID + session.roomid + pswd + session.salt, 6).then(function (hash) {
var volume = getStorage("micVolume_" + hash);
if (volume !== "") {
if (parseInt(volume) === 0) {
getById("header").classList.add("orange");
getById("head7").classList.remove("hidden");
} else if (parseInt(volume)) {
getById("header").classList.remove("orange");
getById("head7").classList.add("hidden");
} else {
return;
}
session.audioGain = parseInt(volume);
var vol = parseFloat(session.audioGain / 100) || 0;
for (var waid in session.webAudios) {
// TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (trackid === wa.id){..
log("Adjusting Gain; only track 0 in all likely hood, unless more than track 0 support is added.");
session.webAudios[waid].gainNode.gain.setValueAtTime(vol, session.webAudios[waid].audioContext.currentTime);
}
}
});
}
}
function hideHomeCheck() {
if (session.hidehome) {
getById("logoname").classList.add("permahide");
getById("container-1").classList.add("permahide");
getById("container-4").classList.add("permahide");
getById("dropButton").classList.add("permahide");
getById("head1").classList.add("permahide");
if (session.permaid === false && session.roomid == false && !session.webcamonly && !session.screenshare) {
getById("mainmenu").classList.add("permahide");
} else {
getById("mainmenu").classList.remove("permahide");
}
getById("audioScreenCaptureDocs").classList.add("permahide");
getById("audioScreenCaptureDocs2").classList.add("permahide");
getById("translateButton").classList.add("permahide");
getById("calendarButton").classList.add("permahide");
getById("info").classList.add("permahide");
getById("helpbutton").classList.add("permahide");
}
if (urlParams.has("headertitle")) {
let pageTitle = urlParams.get("headertitle") || "";
pageTitle = decodeURIComponent(pageTitle) || "";
document.title = pageTitle;
getById("metaTitle").content = pageTitle;
}
if (urlParams.has("favicon")) {
let favicon = "";
if (urlParams.get("favicon")) {
favicon = decodeURIComponent(urlParams.get("favicon")) || "";
}
getById("favicon1").href = favicon;
getById("favicon2").href = favicon;
getById("favicon3").href = favicon;
}
}
function stashRoomSession(broadcastFlag = null) {
try {
let settings = {};
settings.roomid = session.roomid;
settings.password = session.password;
settings.label = session.label;
settings.trb = session.totalRoomBitrate;
settings.widget = session.widget;
settings.codecGroupFlag = session.codecGroupFlag;
settings.showDirector = session.showDirector;
if (broadcastFlag !== null) {
settings.broadcast = broadcastFlag;
}
setStorage("directorOtherSettings", settings);
} catch (e) {
errorlog(e);
}
}
// toggleQualityDirector(1200, this.dataset.UUID, this)
function switchModes(state = null) {
if (state === null) {
session.switchMode = !session.switchMode;
} else {
session.switchMode = state;
}
if (session.switchMode) {
getById("directorlayout").classList.add("hidden");
getById("gridlayout").classList.remove("hidden");
updateMixer();
} else {
getById("directorlayout").classList.remove("hidden");
getById("gridlayout").classList.add("hidden");
for (var UUID in session.rpcs) {
if (session.rpcs[UUID].videoElement) {
session.rpcs[UUID].videoElement.style = "";
session.rpcs[UUID].videoElement.alreadyAdded = false;
var target = document.querySelector("#container_" + UUID + " .controlVideoBox");
if (target) {
target.prepend(session.rpcs[UUID].videoElement);
if (session.signalMeter) {
if (session.rpcs[UUID].signalMeter) {
target.appendChild(session.rpcs[UUID].signalMeter);
}
}
if (session.batteryMeter) {
if (session.rpcs[UUID].batteryMeter) {
target.appendChild(session.rpcs[UUID].batteryMeter);
}
}
if (session.rpcs[UUID].voiceMeter) {
target.appendChild(session.rpcs[UUID].voiceMeter);
}
if (session.rpcs[UUID].remoteMuteElement) {
target.appendChild(session.rpcs[UUID].remoteMuteElement);
}
}
}
}
if (session.videoElement) {
session.videoElement.style = "";
session.videoElement.alreadyAdded = false;
if (session.showDirector == true) {
var target = document.querySelector("#videoContainer_director");
if (target && session.videoElement) {
target.prepend(session.videoElement);
}
} else if ((session.videoElement.srcObject && session.videoElement.srcObject.getTracks().length) || getById("press2talk").dataset.enabled == true) {
getById("miniPerformer").prepend(session.videoElement);
}
}
if (session.screenShareElement) {
session.screenShareElement.style = "";
session.screenShareElement.alreadyAdded = false;
if (session.showDirector == true) {
var target = document.querySelector("#videoScreenContainer_director");
if (target && session.screenShareElement && session.screenShareElement.srcObject && session.screenShareElement.srcObject.getTracks().length) {
target.prepend(session.screenShareElement);
}
} else if ((session.screenShareElement.srcObject && session.screenShareElement.srcObject.getTracks().length) || getById("press2talk").dataset.enabled == true) {
getById("miniPerformer").prepend(session.videoElement);
}
}
applyQualityDirector();
}
}
var updateMixerTimer = null;
var updateMixerActive = false;
function updateMixer(e = false) {
var controlBar = document.getElementById("subControlButtons");
if (controlBar && controlBar.dragElement && !controlBar.isDragging) {
let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
// Calculate real boundaries (parent position: fixed issues)
let topOffset = 0;
let leftOffset = 0;
let elementOffset = controlBar;
while (elementOffset) {
topOffset += elementOffset.offsetTop;
leftOffset += elementOffset.offsetLeft;
elementOffset = elementOffset.offsetParent;
}
let realX = controlBar.xOffset + leftOffset;
let realY = controlBar.yOffset + topOffset;
let maxX = vw - controlBar.offsetWidth;
let maxY = vh - controlBar.offsetHeight;
if (realX > maxX) {
controlBar.xOffset = maxX - leftOffset;
} else if (realX < 0) {
controlBar.xOffset = 0 - leftOffset;
}
if (realY > maxY) {
controlBar.yOffset = maxY - topOffset;
} else if (realY < 0) {
controlBar.yOffset = 0 - topOffset;
}
controlBar.style.transform = `translate(${controlBar.xOffset}px, ${controlBar.yOffset}px)`;
}
if (session.manual === true) {
return;
} else if (!session.switchMode && session.director) {
return;
} else if (session.windowed) {
return;
}
clearInterval(updateMixerTimer);
if (updateMixerActive) {
if (session.mobile) {
updateMixerTimer = setTimeout(function () {
updateMixer();
}, 200);
} else {
updateMixerTimer = setTimeout(function () {
updateMixer();
}, 50);
}
return;
}
updateMixerActive = true;
log("updating mixer");
try {
updateMixerRun(e);
} catch (e) {}
if (session.mobile) {
setTimeout(function () {
updateMixerActive = false;
}, 500);
} else {
setTimeout(function () {
updateMixerActive = false;
}, 100);
}
}
function updateMixerRun(e = false) {
// this is the main auto-mixing code. It's a giant function that runs when there are changes to screensize, video track statuses, etc.
try {
if (session.switchMode) {
} else if (session.director) {
return;
} else if (session.manual === true) {
return;
}
var header = getById("header");
var playarea = getById("gridlayout");
if (session.pipWindow) {
var hi = 0;
var h = session.pipWindow.clientHeight || session.pipWindow.innerHeight || session.pipWindow.outerHeight;
var w = session.pipWindow.clientWidth || session.pipWindow.innerWidth || session.pipWindow.outerWidth;
} else if (document.body.dataset.rotated) {
var hi = header.offsetHeight;
var w = document.body.clientHeight;
if (session.widget && session.iFramesAllowed) {
w *= (100-session.widgetwidth)/100;
try {
let widget = document.getElementById("widget");
if (!widget) {
widget = document.createElement("iframe");
widget.id = "widget";
widget = loadIframe(parseURL4Iframe(session.widget), widget);
if (widget){
document.body.appendChild(widget);
if (session.widgetleft){
widget.classList.add("left");
playarea.style.left = session.widgetwidth+"%";
playarea.style.width = (100 - session.widgetwidth)+"%";
} else {
playarea.style.left = "0";
playarea.style.width = (100 - session.widgetwidth)+"%";
}
}
}
if (widget){
widget.style.height = "calc(100% - " + hi + "px)";
widget.style.top = hi;
}
} catch (e) {
errorlog(e);
}
} else if (!session.widget && session.widgetleft){
playarea.style.left = "0";
}
var h = document.body.clientWidth - hi;
if (session.dedicatedControlBarSpace || document.body.clientWidth <= 700) {
// # This needs to be reviewed.
if (session.dedicatedControlBarSpace !== false) {
let controlBar = document.getElementById("subControlButtons");
if (controlBar && !session.overlayControls) {
if (!controlBar.yOffset || controlBar.yOffset > -10) {
h = document.body.clientWidth - hi - controlBar.offsetHeight;
}
}
}
}
} else {
var hi = header.offsetHeight;
var w = window.innerWidth;
if (session.widget && session.iFramesAllowed) {
w *= (100 - session.widgetwidth)/100;
try {
let widget = document.getElementById("widget");
if (!widget) {
widget = document.createElement("iframe");
widget.id = "widget";
widget = loadIframe(parseURL4Iframe(session.widget), widget);
if (widget){
document.body.appendChild(widget);
if (session.widgetleft){
widget.classList.add("left");
playarea.style.left = session.widgetwidth+"%";
playarea.style.width = (100 - session.widgetwidth)+"%";
playarea.style.position = "absolute";
} else {
playarea.style.left = "0";
playarea.style.width = (100 - session.widgetwidth)+"%";
}
}
}
if (widget){
widget.style.height = "calc(100% - " + hi + "px)";
widget.style.top = hi;
}
} catch (e) {
errorlog(e);
}
} else if (!session.widget && session.widgetleft){
playarea.style.left = "0";
}
var h = window.innerHeight - hi;
if (session.dedicatedControlBarSpace || window.innerHeight <= 700) {
// # This needs to be reviewed.
if (session.dedicatedControlBarSpace !== false) {
let controlBar = document.getElementById("subControlButtons");
if (controlBar && !session.overlayControls) {
if (!controlBar.yOffset || controlBar.yOffset > -10) {
h = window.innerHeight - hi - controlBar.offsetHeight;
}
}
}
}
}
if (session.locked) {
var w123 = w;
var h123 = h;
if (w > h * session.locked) {
w = h * session.locked;
} else if (h > w / session.locked) {
h = w / session.locked;
}
playarea.style.left = (w123 - w) / 2 + "px";
playarea.style.top = (h123 - h) / 2 + "px";
playarea.style.width = w + "px";
playarea.style.height = h + "px";
playarea.style.position = "absolute";
playarea.style.display = "block";
}
var arW = 16.0;
var arH = 9.0;
if (session.aspectRatio) {
if (session.aspectRatio == 1) {
arW = 9.0;
arH = 16.0;
} else if (session.aspectRatio == 2) {
arW = 12.0; // square root; cause why not.
arH = 12.0;
} else if (session.aspectRatio == 3) {
arW = 12.0; // square root; cause why not.
arH = 9.0;
}
}
var groups = [...session.group];
if (session.groupView.length) {
groups.push(...session.groupView);
}
var sssid = false;
var soloVideo = false;
if (session.infocus === true) {
soloVideo = true;
} else if (session.infocus && session.infocus in session.rpcs) {
// if the infocus stream is connected
if (groups.length || session.allowNoGroup) {
try {
if (groups.some(item => session.rpcs[session.infocus].group.includes(item))) {
soloVideo = session.infocus;
}
} catch (e) {
errorlog(e);
}
} else {
soloVideo = session.infocus;
}
} else if (session.infocus2 === true) {
sssid = session.streamID;
} else if (session.infocus2 && session.infocus2 in session.rpcs) {
// if the infocus2 stream is connected
if (groups.length || session.allowNoGroup) {
try {
if (groups.some(item => session.rpcs[session.infocus2].group.includes(item))) {
sssid = session.rpcs[session.infocus2].streamID;
}
} catch (e) {
errorlog(e);
}
} else {
sssid = session.rpcs[session.infocus2].streamID;
}
}
var ww = w / arW;
var hh = h / arH;
var mediaPool = [];
var mediaPool_invisible = [];
var miniPreview = session.minipreview;
if (miniPreview && (!session.activeSpeaker && session.layout) && session.streamID && session.streamID in session.layout) {
miniPreview = false;
if (session.videoElement.container && session.videoElement.container.id == "minipreview") {
delete session.videoElement.container;
}
if (document.getElementById("minipreview")) {
document.getElementById("minipreview").remove();
}
}
if (session.iframeEle && session.iframeEle.style.display !== "none") {
// local feed
if (session.order !== false) {
session.iframeEle.order = session.order;
} else {
session.iframeEle.order = 0;
}
if (session.activeSpeaker && !session.activelySpeaking) {
mediaPool_invisible.push(session.iframeEle);
} else {
mediaPool.push(session.iframeEle);
}
}
if (session.videoElement && (session.videoElement.src || session.videoElement.srcObject)) {
// I, myself, exist
if (session.videoElement.style.display !== "none") {
// local feed
if (miniPreview && soloVideo !== true) {
} else {
if (session.order !== false) {
session.videoElement.order = session.order;
} else {
session.videoElement.order = 0;
}
if (session.activeSpeaker && !session.activelySpeaking) {
//mediaPool_invisible.push(session.videoElement);
//} else if (session.videoElement && session.videoElement.srcObject && (session.videoElement.srcObject.getTracks().length === 0)){
// do not show a video element if its completely empty.
} else if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length === 0) {
// do not show a video element if its completely empty.
} else if (soloVideo && soloVideo !== true) {
//
} else if (session.videoMuted && session.style === 1) {
// i'm too tired to try to get this working.
} else {
mediaPool.push(session.videoElement);
}
}
}
}
if (session.screenShareState && session.screenShareElement) {
// I, myself, exist
if (!session.screenShareElementHidden) {
if (session.order !== false) {
session.screenShareElement.order = session.order;
} else {
session.screenShareElement.order = 0;
}
if (soloVideo !== false) {
//session.screenShareElement.style.display="none";
} else if (session.activeSpeaker && !session.activelySpeaking) {
//session.screenShareElement.style.display="none";
} else {
mediaPool.push(session.screenShareElement);
}
}
}
var delayedRequestList = {};
function delayedRequestRate(bandwidth, UUID, optimizeAudio = false, lock = null) {
delayedRequestList[UUID] = [bandwidth, UUID, optimizeAudio, lock];
}
if (soloVideo && soloVideo in session.rpcs) { // this technically can be a scene or guest
// remote guest being full screened; infocus == UUID
mediaPool = []; // remove myself from fullscreen
if (iOS || iPad) {
if (!miniPreview) {
miniPreview = 1;
}
}
for (var j in session.rpcs) {
if (groups.length || session.allowNoGroup) {
try {
if (!groups.some(item => session.rpcs[j].group.includes(item))) {
continue;
}
} catch (e) {
errorlog(e);
}
}
if (j != soloVideo) {
// this remote guest is NOT in focus
try {
if (session.rpcs[j].iframeEle) {
mediaPool_invisible.push(session.rpcs[j].iframeEle);
}
if (session.rpcs[j].videoElement && session.rpcs[j].videoElement.style.display !== "none") {
// Add it if not hidden
if (session.scene!==false){
//delayedRequestRate(session.hiddenSceneViewBitrate, j);
} else {
if (document.pictureInPictureElement && document.pictureInPictureElement.id && document.pictureInPictureElement.id == session.rpcs[j].videoElement.id) {
var bitratePIP = parseInt(session.zoomedBitrate / 4);
//warnUser("GOOD");
delayedRequestRate(bitratePIP, j);
} else {
delayedRequestRate(0, j); // disable the video of non-fullscreen videos
}
}
} else if (session.rpcs[j].videoElement) {
delayedRequestRate(0, j, true); // disable the video of non-fullscreen videos
}
} catch (e) {
errorlog(e);
}
} else {
// remote guest is in-focus video
////////
try {
if (session.rpcs[j].iframeEle) {
mediaPool_invisible.push(session.rpcs[j].iframeEle);
}
if (session.rpcs[j].videoElement) {
mediaPool.push(session.rpcs[j].videoElement); // active speaker
session.rpcs[j].videoElement.style.visibility = "visible";
if (session.rpcs[j].order !== false) {
session.rpcs[j].videoElement.order = session.rpcs[j].order;
} else {
session.rpcs[j].videoElement.order = 0;
}
if (session.scene!==false){
} else {
var totalRoomBitrate = session.totalRoomBitrate;
if (session.controlRoomBitrate !== false && session.controlRoomBitrate !== true) {
totalRoomBitrate = Math.min(session.controlRoomBitrate, totalRoomBitrate);
}
var targetBitrate = session.zoomedBitrate;
if (totalRoomBitrate > session.zoomedBitrate) {
targetBitrate = totalRoomBitrate;
}
delayedRequestRate(targetBitrate, j); // 1.2-Mbps is decent, no? in-focus, so higher bitrate
}
}
} catch (e) {
errorlog(e);
}
}
}
} else if (soloVideo && soloVideo === true) { // this cannot be a scene, as you can't have yourself in a scene.
// well, fullscreen myself. "true" represents me. UUID would be for others.
// already added myself to this as fullscreen
for (var j in session.rpcs) {
if (groups.length || session.allowNoGroup) {
try {
if (!groups.some(item => session.rpcs[j].group.includes(item))) {
continue;
}
} catch (e) {
errorlog(e);
}
}
try {
if (session.rpcs[j].videoElement && session.rpcs[j].videoElement.style.display !== "none") {
// Add it if not hidden
if (document.pictureInPictureElement && document.pictureInPictureElement.id && document.pictureInPictureElement.id == session.rpcs[j].videoElement.id) {
var bitratePIP = parseInt(session.zoomedBitrate / 4);
delayedRequestRate(bitratePIP, j);
//warnUser("GOOD");
} else {
delayedRequestRate(0, j); // disable the video of non-fullscreen videos
}
// mediaPool_invisible.push(session.rpcs[j].videoElement);
} else if (session.rpcs[j].videoElement) {
delayedRequestRate(0, j, true); // other videos are disabled when previewing yourself, but audio retained
}
} catch (e) {
errorlog(e);
}
}
} else {
var roomQuality = 0;
var screenShareTotal = 0;
for (var i in session.rpcs) {
if (session.rpcs[i] === null) {
continue;
}
if (groups.length || session.allowNoGroup) {
try {
if (!groups.some(item => session.rpcs[i].group.includes(item))) {
continue;
}
} catch (e) {
errorlog(e);
}
}
if (session.rpcs[i].videoElement) {
// remote feeds
if (session.rpcs[i].videoElement.style.display !== "none") {
if (session.rpcs[i].videoElement.srcObject && session.rpcs[i].videoElement.srcObject.getVideoTracks().length) {
// only count videos with actual video tracks; audio-only excluded
if (session.rpcs[i].videoMuted) {
// it's video muted
// mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on
} else if (session.rpcs[i].directorVideoMuted) {
// it's muted by the director, so likely disabled.
// mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on
} else if (session.rpcs[i].virtualHangup) {
} else if (session.rpcs[i].bandwidthMuted) {
} else if (session.rpcs[i].videoElement.style.opacity === "0") {
// mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on
} else {
roomQuality += 1;
if (session.rpcs[i].screenShareState) {
screenShareTotal += 1;
}
}
}
}
}
}
if (session.broadcast !== false) {
if ((!session.activeSpeaker && session.layout) && session.streamID in session.layout) {
// skip
} else {
if (roomQuality > 0) {
if (session.nopreview !== false) {
for (var i = 0; i < mediaPool.length; i++) {
if (mediaPool[i].nodeName && mediaPool[i].nodeName == "IFRAME") {
mediaPool[i].style.display = "none";
}
}
mediaPool = []; // we don't want to show our self-preview if in broadcast mode and there is a director.
}
}
}
}
if (roomQuality === 0) {
roomQuality = 1;
}
var totalRoomBitrate = session.totalRoomBitrate;
if (session.controlRoomBitrate !== false && session.controlRoomBitrate !== true) {
totalRoomBitrate = Math.min(session.controlRoomBitrate, totalRoomBitrate);
}
var roomBitrate = totalRoomBitrate;
var sceneBitrate = false;
var screenShareBitrate = false;
if (session.bitrate && !roomBitrate && !session.totalSceneBitrate) {
roomBitrate = session.bitrate;
} else if (session.screenShareBitrate !== false) {
screenShareBitrate = session.screenShareBitrate;
if (roomQuality - screenShareTotal > 0) {
roomBitrate = parseInt(totalRoomBitrate / (roomQuality - screenShareTotal));
if (session.totalSceneBitrate) {
sceneBitrate = parseInt(session.totalSceneBitrate / (roomQuality - screenShareTotal));
if (session.bitrate !== false) {
sceneBitrate = Math.min(session.bitrate, sceneBitrate);
}
}
}
} else if (screenShareTotal) {
try {
if (session.roomid !== false && session.scene === false) {
if (roomQuality - screenShareTotal <= 0) {
roomBitrate = totalRoomBitrate;
screenShareBitrate = totalRoomBitrate;
} else {
screenShareBitrate = totalRoomBitrate / (1.5 * screenShareTotal);
roomBitrate = parseInt((totalRoomBitrate - screenShareBitrate) / (roomQuality - screenShareTotal));
}
} else if (session.totalSceneBitrate !== false) {
if (roomQuality - screenShareTotal <= 0) {
sceneBitrate = session.totalSceneBitrate;
if (session.bitrate !== false) {
sceneBitrate = Math.min(session.bitrate, sceneBitrate);
}
screenShareBitrate = sceneBitrate;
} else {
screenShareBitrate = parseInt(totalRoomBitrate / (1.5 * screenShareTotal));
sceneBitrate = parseInt((totalRoomBitrate - screenShareBitrate) / (roomQuality - screenShareTotal));
if (session.bitrate !== false) {
sceneBitrate = Math.min(session.bitrate, sceneBitrate);
screenShareBitrate = Math.min(session.bitrate, screenShareBitrate);
}
}
} else {
screenShareBitrate = false;
}
} catch (e) {
errorlog(e);
}
} else {
roomBitrate = parseInt(totalRoomBitrate / roomQuality);
if (session.totalSceneBitrate) {
sceneBitrate = parseInt(session.totalSceneBitrate / roomQuality);
if (session.bitrate !== false) {
sceneBitrate = Math.min(session.bitrate, sceneBitrate);
}
}
}
if (session.minimumRoomBitrate) {
if (session.totalRoomBitrate && roomBitrate < session.minimumRoomBitrate) {
roomBitrate = session.minimumRoomBitrate;
if (roomBitrate > session.totalRoomBitrate) {
roomBitrate = session.totalRoomBitrate;
}
}
if (session.totalSceneBitrate && sceneBitrate < session.minimumRoomBitrate) {
sceneBitrate = session.minimumRoomBitrate;
if (sceneBitrate > session.totalSceneBitrate) {
sceneBitrate = session.totalSceneBitrate;
}
}
}
var i = null;
var countOrder = 0;
try {
var RPCSkeys = Object.keys(session.rpcs); // default sorting type: time added; //RPCSkeys.sort();
} catch (e) {
return;
}
for (var keyIndex = 0; keyIndex < RPCSkeys.length; keyIndex++) {
i = RPCSkeys[keyIndex];
if (session.rpcs[i] === null) {
continue;
}
session.rpcs[i].mutedStateMixer = false;
if (groups.length || session.allowNoGroup) {
// The MAIN and LAST group filter.
try {
if (!groups.some(item => session.rpcs[i].group.includes(item))) {
if (session.scene !== false) {
if (session.groupAudio) {
delayedRequestRate(session.hiddenSceneViewBitrate, i, false);
} else {
delayedRequestRate(session.hiddenSceneViewBitrate, i, true); // hidden. I dont want it to be super low, for video quality reasons.
session.rpcs[i].mutedStateMixer = true;
}
if (!session.hiddenSceneViewBitrate) {
session.rpcs[i].videoElement.nogb = 2;
}
} else {
if (session.groupAudio) {
delayedRequestRate(0, i, false);
} else {
delayedRequestRate(0, i, true); // w/e This is not in OBS, so we just set it as low as possible. Shoudln't exist really unless loading?
session.rpcs[i].mutedStateMixer = true;
}
}
applyMuteState(i);
continue;
}
} catch (e) {}
}
applyMuteState(i);
var doNotPush = false;
if (session.rpcs[i].iframeEle) {
if (session.rpcs[i].iframeEle.style.display == "none") {
// pass
} else if (session.rpcs[i].iframeEle.style.opacity === "0") {
// pass
} else {
session.rpcs[i].iframeEle.style.visibility = "visible";
if (session.rpcs[i].order !== false) {
session.rpcs[i].iframeEle.order = session.rpcs[i].order;
} else {
session.rpcs[i].iframeEle.order = 0;
}
try {
if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
mediaPool_invisible.push(session.rpcs[i].iframeEle); // TODO: this needs validation; will the iframe be maintained if activer speaker is going? do we even want this?
/* } else if (session.rpcs[i].iframeEle.dataset.meshcast){ //////// MESH CAST ONLY LOGIC
if (session.rpcs[i].iframeEle.contentDocument && session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){
if (session.rpcs[i].iframeVideo){
mediaPool.push(session.rpcs[i].iframeVideo);
} else if (session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){
session.rpcs[i].iframeVideo = session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video")[0];
session.rpcs[i].iframeVideo.id="meshcast_"+i;
//errorlog("THIS IS GOOD");
mediaPool.push(session.rpcs[i].iframeVideo);
} else {
//errorlog("No video yet");
}
} else { // this is a problem is not on the same domain.
if (!document.getElementById("iframe_"+i)){
if (document.getElementById("hiddenElements")){
document.getElementById("hiddenElements").append(session.rpcs[i].iframeEle);
} else {
document.body.append(session.rpcs[i].iframeEle);
}
if (session.rpcs[i].iframeVideo){
mediaPool.push(session.rpcs[i].iframeVideo);
} else if (session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){
session.rpcs[i].iframeVideo = session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video")[0];
session.rpcs[i].iframeVideo.id="meshcast_"+i;
mediaPool.push(session.rpcs[i].iframeVideo);
} else {
//errorlog("No video yet");
}
} else {
if (session.rpcs[i].iframeVideo){
mediaPool.push(session.rpcs[i].iframeVideo);
} else {
//errorlog("Does not support contentDocument or something");
}
}
} */
} else {
///////// MESH CAST LOGIC ENDS HERE
//errorlog("not meshcast");
mediaPool.push(session.rpcs[i].iframeEle);
}
} catch (e) {
errorlog(e);
}
}
}
if (session.rpcs[i].imageElement) {
//if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.srcObject.getAudioTracks().length) {
// is there audio?
// mediaPool_invisible.push(session.rpcs[i].videoElement); // include audio as hidden track;
//}
if (session.rpcs[i].videoMuted || session.rpcs[i].directorVideoMuted || session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted) {
continue;
}
if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.style.display == "none") {
// currently this is considered the state of scenes. pertty dumb on my part.
continue;
}
if (session.rpcs[i].order !== false) {
session.rpcs[i].imageElement.order = session.rpcs[i].order;
} else {
session.rpcs[i].imageElement.order = 0;
}
if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
// mediaPool_invisible.push(session.rpcs[i].imageElement);
} else {
mediaPool.push(session.rpcs[i].imageElement);
}
doNotPush = true;
}
if (session.rpcs[i].videoElement) {
// remote feeds
//session.rpcs[i].targetBandwidth = -1;
if (session.rpcs[i].videoElement.style.opacity === "0") {
continue;
}
try {
session.rpcs[i].videoElement.style.visibility = "visible";
} catch (e) {
errorlog(e);
}
if (session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted || session.rpcs[i].directorVideoMuted) {
continue;
}
if (session.style && session.style >= 2) {
if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) {
if (session.rpcs[i].videoElement.style.display == "none") {
// currently this is considered the state of scenes. pertty dumb on my part.
continue;
}
if (createStyleCanvas(i)) {
applyStyleEffect(i);
}
if (session.rpcs[i].order !== false) {
session.rpcs[i].canvas.order = session.rpcs[i].order;
} else {
session.rpcs[i].canvas.order = 0;
}
if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
// mediaPool_invisible.push(session.rpcs[i].canvas);
} else {
mediaPool.push(session.rpcs[i].canvas);
}
doNotPush = true;
//continue;
}
} else if (session.style == 1) {
if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) {
//if (session.style==1){ // avatars and waveforms might be better done elsewhere? as a canvas effect even?
doNotPush = true;
//}
}
} else if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) {
if (session.rpcs[i].screenShareState) {
doNotPush = true;
}
}
//} else if (!session.directorList.indexOf(i)>=0){ // director is never audio-only. Video if need, yes, but not visualized-audio.
// if (session.rpcs[i].videoElement.srcObject && ((session.rpcs[i].videoElement.srcObject.getVideoTracks().length==0) || (session.rpcs[i].videoMuted)) && !session.rpcs[i].directorVideoMuted){
// continue;
// }
//}
session.rpcs[i].opacityMuted = "1";
if (session.rpcs[i].opacityDisconnect == "1") {
if (session.rpcs[i].videoElement) {
session.rpcs[i].videoElement.style.opacity = "1";
}
}
if (session.rpcs[i].videoMuted) {
if (session.rpcs[i].videoElement.srcObject.getAudioTracks().length == 0) {
// if no audio track, no point in removing the video track, since it will just stall out then.
continue; // easiest is to just not show anything if no video and no audio track.
}
if (session.rpcs[i].videoElement.srcObject) {
session.rpcs[i].videoElement.srcObject.getVideoTracks().forEach(track => {
session.rpcs[i].videoElement.srcObject.removeTrack(track);
session.rpcs[i].videoElement.load();
});
}
//continue; // currently disabling this, since we want to show it.
} else if (session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted || session.rpcs[i].directorVideoMuted) {
continue;
}
if (session.scene !== false) {
if (session.sceneType === 3) {
// order
countOrder += 1;
if (session.order === false) {
if (countOrder == 1) {
session.rpcs[i].videoElement.style.display = "block";
} else {
session.rpcs[i].videoElement.style.display = "none";
}
} else if (session.order === countOrder) {
session.rpcs[i].videoElement.style.display = "block";
} else {
session.rpcs[i].videoElement.style.display = "none";
}
}
}
if (session.rpcs[i].videoElement.style.display == "none") {
// Video is disabled; run at lowest
if (session.scene !== false) {
delayedRequestRate(session.hiddenSceneViewBitrate, i, true); // hidden. I dont want it to be super low, for video quality reasons.
if (!session.hiddenSceneViewBitrate) {
session.rpcs[i].videoElement.nogb = 2;
}
} else {
delayedRequestRate(0, i, true); // w/e This is not in OBS, so we just set it as low as possible. Shoudln't exist really unless loading?
}
} else if (session.scene !== false) {
// max
//
if (sceneBitrate !== false) {
if (screenShareBitrate !== false && session.rpcs[i].screenShareState) {
delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality.
} else {
delayedRequestRate(sceneBitrate, i); // well, screw that. Setting it to room quality.
}
} else {
if (screenShareBitrate !== false && session.rpcs[i].screenShareState) {
delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality.
} else {
delayedRequestRate(-1, i); // unlock.
}
}
if (session.rpcs[i].order !== false) {
session.rpcs[i].videoElement.order = session.rpcs[i].order;
} else {
session.rpcs[i].videoElement.order = 0;
}
if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
if (!(session.rpcs[i].videoElement in mediaPool_invisible)) {
// mediaPool_invisible.push(session.rpcs[i].videoElement);
} else {
errorlog("THIS SHOULD NOT HAPPEN; 650");
}
} else if (!doNotPush) {
mediaPool.push(session.rpcs[i].videoElement);
}
} else if (session.roomid !== false) {
// guests should see video at low bitrate, ie: 100kbps (not 35kbps like if disabled)
if (session.rpcs[i].order !== false) {
session.rpcs[i].videoElement.order = session.rpcs[i].order;
} else {
session.rpcs[i].videoElement.order = 0;
}
if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
if (!(session.rpcs[i].videoElement in mediaPool_invisible)) {
// mediaPool_invisible.push(session.rpcs[i].videoElement);
} else {
errorlog("THIS SHOULD NOT HAPPEN; 665");
}
} else if (!doNotPush) {
mediaPool.push(session.rpcs[i].videoElement);
}
if (session.roomid === "" && session.bitrate) {
// we will let the URL specified bitrate hold, since this isn't a real room.
delayedRequestRate(-1, i);
} else {
if (screenShareBitrate !== false && session.rpcs[i].screenShareState) {
delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality.
} else {
delayedRequestRate(roomBitrate, i); // well, screw that. Setting it to room quality.
}
}
} else {
// view=xx,yy or whatever. This should be highest quality.
if (session.rpcs[i].order !== false) {
session.rpcs[i].videoElement.order = session.rpcs[i].order;
} else {
session.rpcs[i].videoElement.order = 0;
}
if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) {
if (!(session.rpcs[i].videoElement in mediaPool_invisible)) {
// mediaPool_invisible.push(session.rpcs[i].videoElement);
} else {
errorlog("THIS SHOULD NOT HAPPEN; 684");
}
} else if (!doNotPush) {
mediaPool.push(session.rpcs[i].videoElement);
}
if (sceneBitrate) {
delayedRequestRate(sceneBitrate, i);
} else if (session.screenShareBitrate !== false && session.rpcs[i].screenShareState) {
// session.screenShareBitrate is non-room
delayedRequestRate(session.screenShareBitrate, i); // well, screw that. Setting it to room quality.
} else {
delayedRequestRate(-1, i);
}
}
if (session.rpcs[i].videoElement.nogb == 2) {
session.rpcs[i].videoElement.nogb = 1;
session.rpcs[i].videoElement.classList.add("nogb");
} else if (session.rpcs[i].videoElement.nogb == 1) {
session.rpcs[i].videoElement.nogb = 0;
session.rpcs[i].videoElement.classList.remove("nogb");
}
}
}
}
if (session.broadcastIFrame && session.broadcastIFrame.src) {
// keep alive iframes whennot visible. i think
if (!mediaPool.length) {
mediaPool.push(session.broadcastIFrame);
}
}
if (document.fullscreenElement) {
try {
if (document.fullscreenElement.tagName === "VIDEO") {
// if its HTML, than we assume its the full canvas
for (var i = 0; i < mediaPool.length; i++) {
// if its your local camera, it shouldn't be a problem, so we can focus on remote cameras only
if (mediaPool[i].id !== document.fullscreenElement.id) {
// if its selected camera, we want to exclude it
if (mediaPool[i].dataset && mediaPool[i].dataset.UUID && mediaPool[i].tagName && mediaPool[i].tagName == "VIDEO") {
delayedRequestRate(session.hiddenSceneViewBitrate, mediaPool[i].dataset.UUID, null); // null implies don't change the current audio setting
mediaPool_invisible.push(mediaPool[i]); // move visible elements to the invisible list, since something is full screen
mediaPool.splice(i, 1);
}
}
}
}
} catch (e) {
errorlog(e);
}
}
var sscount = 0;
var skip = false;
for (var m = 0; m < mediaPool.length; m++) {
mediaPool[m].alreadyAdded = false;
}
if (!session.layout || session.activeSpeaker) {
if (session.orderby) {
if (session.orderby == "id") {
mediaPool.sort(compare_vids_sid);
} else if (session.orderby == "label") {
mediaPool.sort(compare_vids_label);
} else {
mediaPool.sort(compare_vids_sid);
}
}
mediaPool.sort(compare_vids);
} else if (session.exclusiveLayoutAudio) {
[...mediaPool].forEach(ele => {
if (ele.dataset.sid) {
if (session.layout[ele.dataset.sid]) {
return;
} else if (session.layout[""]) {
let matched = false;
session.layout[""].forEach(i => {
if (i.defaultStreamID && i.defaultStreamID == ele.dataset.sid) {
matched = true;
}
});
if (matched) {
return;
}
}
const index = mediaPool.indexOf(ele);
if (index > -1) {
if (ele.dataset.UUID && session.scene !== false) {
delayedRequestRate(session.hiddenSceneViewBitrate, ele.dataset.UUID, false); // it's added already, so we know it needs sound. But lets d
}
mediaPool.splice(index, 1); // 2nd parameter means remove one item only
}
}
});
if (RPCSkeys) {
for (var keyIndex = 0; keyIndex < RPCSkeys.length; keyIndex++) {
i = RPCSkeys[keyIndex];
if (session.rpcs[i] === null) {
continue;
}
if (session.rpcs[i].streamID) {
let matched = false;
mediaPool.forEach(ele => {
if (ele.dataset.sid == session.rpcs[i].streamID) {
matched = true;
}
});
if (!matched) {
if (session.rpcs[i].mutedStateMixer === false) {
session.rpcs[i].mutedStateMixer = true;
applyMuteState(i);
}
}
}
}
}
}
if (session.fakeFeeds && session.fakeFeeds.length && mediaPool.length < session.fakeFeeds.length) {
for (let i = 0; i < session.fakeFeeds.length; i++) {
if (mediaPool.length < session.fakeFeeds.length) {
mediaPool.push(session.fakeFeeds[i]);
} else {
try {
session.fakeFeeds[i].remove();
} catch (e) {
errorlog(e);
}
}
}
}
if (session.slotsList && session.slotsList.length > 0) {
// Filter mediaPool to only include videos in the slotsList
const filteredMediaPool = [];
for (let i = 0; i < mediaPool.length; i++) {
if (session.slotsList.includes(i + 1)) { // +1 for 1-indexed slotsList
filteredMediaPool.push(mediaPool[i]);
}
}
mediaPool = filteredMediaPool;
}
var mpl = session.slots || mediaPool.length;
if (!sssid) {
if (mpl > 1) {
var BB = 0;
var rw = 1;
var rh = 1;
var NW;
var NH;
var current;
for (NW = 1; NW <= mpl; NW++) {
NH = Math.ceil(mpl / NW);
var www = ww / NW;
var hhh = hh / NH;
if (www > hhh) {
current = Math.round(hhh * hhh * (mpl / (NW * NH)));
} else {
current = Math.round(www * www * (mpl / (NW * NH)));
}
if (current >= BB) {
BB = current;
rw = NW;
rh = NH;
}
if (mediaPool[NW - 1]) {
//if (mediaPool[NW-1].tagName == "VIDEO"){
if (mediaPool[NW - 1].dataset.UUID) {
if (mediaPool[NW - 1].dataset.UUID in session.rpcs) {
if (session.rpcs[mediaPool[NW - 1].dataset.UUID].screenShareState && !session.rpcs[mediaPool[NW - 1].dataset.UUID].smallScreen) {
sscount += 1;
sssid = mediaPool[NW - 1].dataset.sid;
}
}
} else if ("id" in mediaPool[NW - 1] && mediaPool[NW - 1].id == "screensharesource" && session.notifyScreenShare) {
sscount += 1;
sssid = mediaPool[NW - 1].dataset.sid;
}
}
}
} else {
var rw = 1;
var rh = 1;
}
if (sscount > 1) {
sssid = false; // lets not maximize if more than one screen share.
}
}
} catch (e) {
errorlog(e);
sssid = false;
}
// Add screen share status classes to the gridlayout element
if (sscount > 0) {
playarea.classList.add("has-screenshare");
playarea.classList.remove("no-screenshare");
} else {
playarea.classList.add("no-screenshare");
playarea.classList.remove("has-screenshare");
}
var customLayout = false;
if (!session.notifyScreenShare && session.scene !== false) {
// this is a scene, so lets assume &smallshare will disable larger screen shares since there is no one to screen share.
} else if (sssid && session.screenshareStyle && (session.screenshareStyle==1) && (!session.layout || session.activeSpeaker)) {
customLayout = {};
if (mediaPool.length >= 12) {
customLayout[sssid] = { x: 10, y: 10, w: 90, h: 90, c: false };
} else if (mediaPool.length >= 10) {
customLayout[sssid] = { x: 0, y: 10, w: 100, h: 90, c: false };
} else if (mediaPool.length >= 8) {
customLayout[sssid] = { x: 20, y: 20, w: 80, h: 80, c: false };
} else if (mediaPool.length == 7) {
customLayout[sssid] = { x: 16.66667, y: 0, w: 83.33333, h: 100, c: false };
} else if (mediaPool.length == 5 || mediaPool.length == 6) {
customLayout[sssid] = { x: 20, y: 0, w: 80, h: 100, c: false };
} else {
customLayout[sssid] = { x: 20, y: 0, w: 80, h: 100, c: false };
}
var posCount = 0;
for (var i = 0; i < mediaPool.length; i++) {
if (mediaPool[i].dataset.sid === sssid) {
continue;
}
if (mediaPool.length == 2) {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: 0, w: 20, h: 100, c: session.cover };
} else if (mediaPool.length == 3) {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 41 + 9, w: 20, h: 41, c: session.cover };
} else if (mediaPool.length == 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 33.3333, w: 20, h: 33.3333, c: session.cover };
} else if (mediaPool.length == 5) {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: session.cover };
} else if (mediaPool.length == 6) {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 20, w: 20, h: 20, c: session.cover };
} else if (mediaPool.length == 7) {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 16.66667, w: 16.66667, h: 16.66667, c: session.cover };
} else if (mediaPool.length == 8) {
if (posCount == 0 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 70, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 1 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 50, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 2 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 30, y: 0, w: 20, h: 20, c: true };
} else {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: true };
}
} else if (mediaPool.length == 9) {
if (posCount == 0 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 1 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 60, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 2 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 40, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 3 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 20, y: 0, w: 20, h: 20, c: true };
} else {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: true };
}
} else if (mediaPool.length >= 10) {
if (posCount < 10) {
customLayout[mediaPool[i].dataset.sid] = { x: 90 - (10 * posCount), y: 0, w: 10, h: 10, c: true };
} else {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: (posCount - 9) * 10, w: 10, h: 10, c: true };
}
} else {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 20, w: 20, h: 20, c: true };
}
posCount += 1;
}
} else if (sssid && (!session.layout || session.activeSpeaker)) {
customLayout = {};
if (mediaPool.length >= 12) {
customLayout[sssid] = { x: 0, y: 10, w: 90, h: 90, c: false };
} else if (mediaPool.length >= 10) {
customLayout[sssid] = { x: 0, y: 10, w: 100, h: 90, c: false };
} else if (mediaPool.length >= 8) {
customLayout[sssid] = { x: 0, y: 20, w: 80, h: 80, c: false };
} else if (mediaPool.length == 7) {
customLayout[sssid] = { x: 0, y: 0, w: 83.33333, h: 100, c: false };
} else if (mediaPool.length == 5) {
customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false };
} else if (mediaPool.length == 6) {
customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false };
} else {
customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false };
}
var posCount = 0;
for (var i = 0; i < mediaPool.length; i++) {
if (mediaPool[i].dataset.sid === sssid) {
continue;
}
if (mediaPool.length == 2) {
//
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: 0, w: 20, h: 100, c: session.cover };
} else if (mediaPool.length == 3) {
//
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 41 + 9, w: 20, h: 41, c: session.cover };
} else if (mediaPool.length == 4) {
//
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 33.3333, w: 20, h: 33.3333, c: session.cover };
} else if (mediaPool.length == 5) {
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: session.cover };
} else if (mediaPool.length == 6) {
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 20, w: 20, h: 20, c: session.cover };
} else if (mediaPool.length == 7) {
customLayout[mediaPool[i].dataset.sid] = { x: 83.33333, y: posCount * 16.66667, w: 16.66667, h: 16.66667, c: session.cover };
} else if (mediaPool.length == 8) {
if (posCount == 0 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 10, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 1 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 30, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 2 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 50, y: 0, w: 20, h: 20, c: true };
} else {
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: true };
}
} else if (mediaPool.length == 9) {
if (posCount == 0 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 0, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 1 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 20, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 2 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 40, y: 0, w: 20, h: 20, c: true };
} else if (posCount == 3 + 4) {
customLayout[mediaPool[i].dataset.sid] = { x: 60, y: 0, w: 20, h: 20, c: true };
} else {
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: true };
}
} else if (mediaPool.length >= 10) {
if (posCount < 10) {
customLayout[mediaPool[i].dataset.sid] = { x: 10 * posCount, y: 0, w: 10, h: 10, c: true };
} else {
customLayout[mediaPool[i].dataset.sid] = { x: 90, y: (posCount - 9) * 10, w: 10, h: 10, c: true };
}
} else {
customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 20, w: 20, h: 20, c: true }; //
}
posCount += 1;
}
} else if (session.rows && mediaPool.length) {
try {
customLayout = {};
let n = mediaPool.length;
if (session.slots && n < session.slots) {
n = session.slots; // If we have fewer videos than slots, still use the full slots count
}
let rows = 1;
if (session.rows.length >= n) {
rows = parseInt(session.rows[n - 1]) || 1;
} else {
rows = parseInt(session.rows[session.rows.length - 1]) || 1;
}
if (rows<0){
rows = Math.abs(rows);
let cols = Math.ceil(n / rows) || 1;
for (var i = 0; i < n; i++) {
let col = i % cols;
let row = parseInt(i / cols) % rows;
if (row === Math.floor((n-1)/cols) && n % cols !== 0) { // Last row logic
let itemsInLastRow = n % cols || cols;
let offset = (cols - itemsInLastRow) * (100 / cols) / 2;
customLayout[mediaPool[i].dataset.sid] = {
y: (100 / rows) * row,
h: 100 / rows,
x: offset + (100 / cols) * col,
w: 100 / cols,
c: session.cover
};
} else {
customLayout[mediaPool[i].dataset.sid] = {
y: (100 / rows) * row,
h: 100 / rows,
x: (100 / cols) * col,
w: 100 / cols,
c: session.cover
};
}
}
} else {
let cols;
if (session.slots) {
cols = session.slots / rows;
} else {
cols = Math.ceil(n / rows) || 1;
}
for (var i = 0; i < n; i++) {
let col = i % cols;
let row = parseInt(i / cols) % rows;
//console.log(row,col, rows,cols,i,n)
if (cols >= 2 && rows >= 2 && n < (row + 1) * cols) {
let delta = (row + 1) * cols - n;
customLayout[mediaPool[i].dataset.sid] = { y: (100 / rows) * row, h: 100 / rows, x: (100 / cols) * (col + delta), w: 100 / cols, c: session.cover };
} else {
customLayout[mediaPool[i].dataset.sid] = { y: (100 / rows) * row, h: 100 / rows, x: (100 / cols) * col, w: 100 / cols, c: session.cover };
}
}
}
} catch (e) {
errorlog(e);
}
}
try {
if (!skip) {
var childNodes = playarea.childNodes;
for (var n = 0; n < childNodes.length; n++) {
if (childNodes[n].querySelector("video")) {
var vidtemp = childNodes[n].querySelector("video");
var matched = false;
for (var m = 0; m < mediaPool.length; m++) {
if (vidtemp.id === mediaPool[m].id) {
vidtemp.alreadyAdded = true;
mediaPool[m] = vidtemp;
matched = true;
childNodes[n].matched = true;
break;
}
}
if (!matched && vidtemp.isInvisible) {
vidtemp.isInvisible = false;
if (session.pauseInvisible && vidtemp.dataset.UUID){
applyMuteState(vidtemp.dataset.UUID);
}
}
} else if (childNodes[n].querySelector("iframe")) {
var iftemp = childNodes[n].querySelector("iframe");
if (iftemp.framesource) {
if ((!session.activeSpeaker && session.layout) && session.layout[""]) {
for (var i = 0; i < session.layout[""].length; i++) {
if (session.layout[""][i].iframeSrc && session.layout[""][i].iframeSrc == iftemp.framesource) {
childNodes[n].matched = true;
if (iftemp.isConnected) {
iftemp.alreadyAdded = true;
}
}
}
}
continue;
}
for (var m = 0; m < mediaPool.length; m++) {
if (mediaPool[m].nodeName === "IFRAME" && mediaPool[m].src && iftemp.src === mediaPool[m].src) {
iftemp.alreadyAdded = true;
iftemp.id = mediaPool[m].id;
if (session.directorList.indexOf(iftemp.dataset.UUID) == -1) {
iftemp.dataset.UUID = mediaPool[m].dataset.UUID;
iftemp.dataset.sid = mediaPool[m].dataset.sid;
}
mediaPool[m] = iftemp;
childNodes[n].matched = true;
break;
}
}
for (var m = 0; m < mediaPool_invisible.length; m++) {
if (mediaPool_invisible[m].nodeName === "IFRAME" && mediaPool_invisible[m].src && iftemp.src === mediaPool_invisible[m].src) {
iftemp.alreadyAdded = true;
iftemp.id = mediaPool_invisible[m].id;
if (session.directorList.indexOf(iftemp.dataset.UUID) == -1) {
iftemp.dataset.UUID = mediaPool_invisible[m].dataset.UUID;
iftemp.dataset.sid = mediaPool_invisible[m].dataset.sid;
}
mediaPool_invisible[m] = iftemp;
childNodes[n].matched = true;
break;
}
}
}
}
for (var n = 0; n < childNodes.length; n++) {
if (!childNodes[n].matched) {
playarea.removeChild(childNodes[n]);
n--;
} else {
childNodes[n].matched = null;
}
}
}
} catch (e) {
errorlog(e);
}
if (session.videoElement && (session.videoElement.src || session.videoElement.srcObject)) {
// fileshare or stream
if ("playlist" in session.videoElement) {
playarea.appendChild(session.videoElement); // fileshare.
} else if (session.videoElement.style.display !== "none") {
if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length) {
if (miniPreview) {
var container = null;
if (mpl === 0 && miniPreview === 2) {
if (soloVideo !== true) {
// since miniPreview==2, we want to full screen our preview. Deleting the old mini preview container will ensure it loads right.
if (!session.layout || session.activeSpeaker) {
if (session.videoElement.container && session.videoElement.container.id == "minipreview") {
delete session.videoElement.container;
}
if (document.getElementById("minipreview")) {
document.getElementById("minipreview").remove();
}
}
//
mediaPool.push(session.videoElement);
mpl = 1;
}
} else if (miniPreview === 3) {
if (soloVideo !== true) {
container = document.createElement("div");
session.videoElement.container = container;
container.style.top = "-500px";
container.style.left = "-500px";
container.style.width = "1px";
container.style.height = "1px";
//container.style.display = "flex";
container.style.zIndex = "0";
container.style.margin = "0";
container.style.position = "absolute";
container.style.cursor = "pointer";
container.style.border = "0";
container.appendChild(session.videoElement);
playarea.appendChild(container);
}
} else if (soloVideo !== true) {
if (document.getElementById("minipreview")) {
container = document.getElementById("minipreview");
} else {
container = document.createElement("div");
var togglePreview = document.createElement("div");
togglePreview.className = "togglePreview";
try {
container.style.top = "calc(" + hi + "px + 2vh)";
container.style.maxHeight = parseInt(playarea.offsetHeight) + "px";
togglePreview.style.top = "calc(" + hi + "px + 2vh)";
togglePreview.style.maxHeight = parseInt(playarea.offsetHeight) + "px";
} catch (e) {
errorlog(e);
container.style.top = hi + "px";
togglePreview.style.top = hi + "px";
}
//
if (miniPerformerY !== null) {
container.style.top = miniPerformerY + "%";
}
if (miniPerformerX !== null) {
if (session.widget && !session.leftMiniPreview) {
if (miniPerformerX > (99-session.widgetwidth)) {
miniPerformerX = 99-session.widgetwidth;
}
}
container.style.left = miniPerformerX + "%";
} else if (session.leftMiniPreview !== false) {
container.style.left = session.leftMiniPreview + "%";
togglePreview.style.left = session.leftMiniPreview + "%";
} else if (session.widget) {
container.style.right = session.widgetwidth+"%";
togglePreview.style.right = session.widgetwidth+"%";
} else {
container.style.right = "2vw";
togglePreview.style.right = "2vw";
}
container.appendChild(session.videoElement);
session.videoElement.container = container;
playarea.appendChild(container);
togglePreview.innerHTML = '';
if (!session.previewToggleState) {
container.classList.toggle("hidden");
togglePreview.classList.toggle("blinded");
}
if (!(iOS || iPad)) {
playarea.appendChild(togglePreview);
}
togglePreview.onclick = function (event) {
event.preventDefault();
event.stopPropagation();
getById("minipreview").classList.toggle("hidden");
this.classList.toggle("blinded");
session.previewToggleState = !session.previewToggleState;
return false;
};
makeMiniDraggableElement(container);
container.id = "minipreview";
}
container.style.width = "18%";
//container.style.display = "flex";
container.style.zIndex = "3";
container.style.margin = "0";
container.style.position = "absolute";
container.style.cursor = "pointer";
container.style.border = "2px #BBB solid";
container.style.height = "block";
applyMirror(session.mirrorExclude);
} else if (soloVideo === true) {
if (document.getElementById("minipreview")) {
container = document.getElementById("minipreview");
container.style.height = "100%";
//container.style.transform = "block";
//container.style.transformOrigin = "unset";
}
}
if (session.ruleOfThirds) {
if (container && container.id == "minipreview" && !container.svg) {
var svg = document.createElement("img");
svg.src = session.ruleOfThirds;
svg.style.width = "100%";
svg.style.height = "100%";
svg.style.position = "absolute";
svg.style.left = "0";
svg.style.top = "0";
container.svg = svg;
container.appendChild(svg);
}
}
container = null; // clear reference
}
} else if (session.streamSrc && !session.videoElement.srcObject) {
warnlog("THIS SHOULD NOT HAPPEN; 2067");
}
}
}
try {
if (session.slots) {
var slotArray = [];
mediaPool.forEach(vid => {
if (vid.slotBlank) {
vid.slotBlank = false;
vid.slot = 0;
}
if ("slot" in vid && vid.slot) {
if (!slotArray.includes(parseInt(vid.slot))) {
slotArray.push(parseInt(vid.slot));
} else {
vid.slot = 0;
//mediaPool_invisible.push(vid);
//var index = mediaPool.indexOf(vid);
//if (index > -1) {
// mediaPool.splice(index, 1);
//}
}
}
});
var slotCounter = 1;
mediaPool.reverse();
var j = mediaPool.length;
while (j--) {
if (!("slot" in mediaPool[j]) || mediaPool[j].slot == "0" || !mediaPool[j].slot) {
while (slotArray.includes(slotCounter)) {
slotCounter += 1;
}
slotArray.push(slotCounter);
mediaPool[j].slot = slotCounter;
mediaPool[j].slotBlank = true;
}
if (!("slot" in mediaPool[j]) || !parseInt(mediaPool[j].slot) || mediaPool[j].slot == "0" || !mediaPool[j].slot || session.slots < parseInt(mediaPool[j].slot)) {
//if ((!("slot" in mediaPool[j]) || !parseInt(mediaPool[j].slot) || mediaPool[j].slot == "0" || !mediaPool[j].slot || session.slots < parseInt(mediaPool[j].slot)) && !customLayout) {
mediaPool_invisible.push(mediaPool[j]);
mediaPool.splice(j, 1);
}
}
mediaPool.reverse();
}
if (session.pauseInvisible){
mediaPool_invisible.forEach(vid => { // This is an experimental version, with improvements? Maybe its better?
if (vid) {
try {
vid.style.width = "";
vid.style.height = "";
if (!vid.isInvisible){
vid.isInvisible = true;
if (session.pauseInvisible && vid.dataset.UUID){
session.requestRateLimit(session.hiddenSceneViewBitrate, vid.dataset.UUID, true);
vid.muted = true;
}
}
if (vid.dataset.doNotMove) {
return;
}
vid.style.top = "0px";
vid.style.left = "0px";
if (vid.isConnected){
console.warn("Video is invisible, yet connected to the DOM?");
}
} catch (e) {
errorlog(e);
}
}
});
} else { // this is the old version; a fail safe.
mediaPool_invisible.forEach(vid => {
if (vid) {
try {
vid.style.width = "0px";
vid.style.height = "0px";
vid.style.top = "0px";
vid.style.left = "0px";
vid.isInvisible = true;
if (vid.alreadyAdded && vid.alreadyAdded == true) {
vid.alreadyAdded = false;
return;
} else if (vid.dataset.doNotMove) {
return;
}
if (!(vid.nodeName=="IFRAME" && vid.isConnected)){
playarea.appendChild(vid);
}
} catch (e) {
errorlog(e);
}
}
});
}
} catch (e) {
errorlog(e);
}
var layout = false;
if (customLayout || (!session.activeSpeaker && session.layout)) {
layout = session.layout || customLayout;
layout = { ...layout };
if (layout[""]) {
for (var i = 0; i < layout[""].length; i++) {
if (layout[""][i].defaultStreamID && !layout[layout[""][i].defaultStreamID]) {
var found = false;
for (var ell in mediaPool) {
if (mediaPool[ell].dataset.sid && mediaPool[ell].dataset.sid === layout[""][i].defaultStreamID) {
layout[layout[""][i].defaultStreamID] = layout[""][i];
found = true;
}
}
if (found) {
continue;
}
}
layout["#" + i] = layout[""][i];
if (layout["#" + i].iframeSrc) {
if (session.iframeSrcs[layout["#" + i].iframeSrc]) {
var ele = session.iframeSrcs[layout["#" + i].iframeSrc];
ele.id = "#" + i;
} else {
var ele = loadIframe(parseURL4Iframe(layout["#" + i].iframeSrc), "#" + i);
ele.framesource = layout["#" + i].iframeSrc;
session.iframeSrcs[layout["#" + i].iframeSrc] = ele;
}
//ele.alreadyAdded = true;
//ele.matched = true;
} else if (layout["#" + i].backgroundMedia || layout["#" + i].text || layout["#" + i].foregroundMedia) {
var ele = document.createElement("div");
ele.dataset.sid = "#" + i;
} else {
continue;
}
ele.dataset.sid = "#" + i;
mediaPool.push(ele);
}
}
try {
mediaPool = sortByZ(mediaPool, layout);
} catch (e) {
// layout = false;
errorlog(e);
}
}
var i = 0;
var offset = 0;
if (session.waitImage && !(mediaPool_invisible.length || mediaPool.length)) {
if (!session.waitImageTimeoutObject) {
session.waitImageTimeoutObject = setTimeout(function () {
session.waitImageTimeoutObject = true;
if (!document.getElementById("retryimage")) {
playarea.innerHTML += '';
getById("retryimage").src = decodeURIComponent(session.waitImage);
getById("retryimage").onerror = function () {
this.style.display = "none";
};
if (session.cover) {
getById("retryimage").style.objectFit = "cover";
}
}
getById("retryimage").style.display = "block";
}, session.waitImageTimeout);
}
} else if (session.waitImage) {
try {
clearTimeout(session.waitImageTimeoutObject);
session.waitImageTimeoutObject = false;
getById("retryimage").style.display = "none";
} catch (e) {}
}
mediaPool.forEach(vid => {
try {
if (!vid || !("id" in vid)) {
errorlog(vid);
return;
}
if (vid.needsLoading) {
try {
vid.load();
} catch (e) {
errorlog(e);
}
}
if (session.slots) {
if ("slot" in vid && parseInt(vid.slot)) {
i = parseInt(vid.slot) - 1;
if (i < 0) {
return;
}
} else {
return;
}
}
var offsetx = 0;
if (i !== 0) {
if (Math.ceil((i + 0.01) / rw) == rh) {
if (mpl % rw) {
offsetx = Math.max(((rw - (mpl % rw)) * (w / rw)) / 2, 0);
}
}
}
var cover = session.cover;
var borderOffset = session.border || 0;
var videoMargin = session.videoMargin || 0;
var borderRadius = session.borderRadius || 0;
var borderColor = session.borderColor || "#000";
var fadein = session.fadein || false;
var backgroundMedia = session.defaultMedia || false;
var foregroundMedia = session.defaultOverlayMedia || false;
var animated = session.animatedMoves || 0;
var textOverlay = false;
if (!borderOffset) {
borderColor = "#0000";
}
if (layout) {
if (!(vid.dataset.sid && vid.dataset.sid in layout)) {
if (vid.container) {
vid.container.style.display = "none";
}
vid.isInvisible = true;
if (session.pauseInvisible && vid.dataset.UUID){
session.requestRateLimit(session.hiddenSceneViewBitrate, vid.dataset.UUID, true);
vid.muted = true;
return;
}
if (vid.dataset.UUID) {
delayedRequestRate(session.hiddenSceneViewBitrate, vid.dataset.UUID, false); // it's added already, so we know it needs sound. But lets d
}
return;
}
if ("borderThickness" in layout[vid.dataset.sid]) {
borderOffset = layout[vid.dataset.sid].borderThickness || 0;
}
if ("animated" in layout[vid.dataset.sid]) {
animated = layout[vid.dataset.sid].animated || 0;
if (animated === true) {
animated = session.animatedMoves || 50;
}
}
if ("margin" in layout[vid.dataset.sid]) {
videoMargin = layout[vid.dataset.sid].margin || 0;
}
if ("rounded" in layout[vid.dataset.sid]) {
borderRadius = layout[vid.dataset.sid].rounded || 0;
}
if (layout[vid.dataset.sid].borderColor) {
borderColor = layout[vid.dataset.sid].borderColor;
}
if (layout[vid.dataset.sid].fadeIn) {
fadein = layout[vid.dataset.sid].fadeIn;
}
if ("backgroundMedia" in layout[vid.dataset.sid]) {
backgroundMedia = layout[vid.dataset.sid].backgroundMedia || false;
}
if ("foregroundMedia" in layout[vid.dataset.sid]) {
foregroundMedia = layout[vid.dataset.sid].foregroundMedia || false;
}
if (layout[vid.dataset.sid].text) {
if (!vid.container || !vid.container.textOverlay) {
textOverlay = document.createElement("div");
textOverlay.className = "textOverlay";
//vid.container.appendChild(vid.container.textOverlay);
} else {
textOverlay = vid.container.textOverlay;
}
textOverlay.innerText = layout[vid.dataset.sid].text;
textOverlay.style.color = layout[vid.dataset.sid].textColor || "#ffffff";
textOverlay.style.fontSize = layout[vid.dataset.sid].fontSize || "24px";
textOverlay.style.fontFamily = layout[vid.dataset.sid].fontFamily || "Arial, sans-serif";
textOverlay.style.position = "absolute";
textOverlay.style.width = "100%";
textOverlay.style.textAlign = "center";
textOverlay.style.zIndex = "10";
// Position the text
const textPosition = layout[vid.dataset.sid].textPosition || "50%";
textOverlay.style.top = textPosition;
textOverlay.style.transform = "translateY(-50%)";
// Add background if specified
if (layout[vid.dataset.sid].textBackground) {
textOverlay.style.backgroundColor = layout[vid.dataset.sid].textBackground;
textOverlay.style.padding = "10px";
} else {
textOverlay.style.backgroundColor = "transparent";
textOverlay.style.textShadow = "1px 1px 2px rgba(0,0,0,0.8)";
}
} else if (vid.container && vid.container.textOverlay) {
vid.container.textOverlay.remove();
delete vid.container.textOverlay;
}
if (vid.container) {
if (!(vid.nodeName == "IFRAME" && vid.isConnected)) {
// moving an iframe will break it.
if (!vid.alreadyAdded || vid.nodeName == "IFRAME") {
playarea.appendChild(vid.container);
}
}
}
}
var skipAnimation = false;
if (vid.isInvisible) {
vid.isInvisible = false;
if (session.pauseInvisible && vid.dataset.UUID){
applyMuteState(vid.dataset.UUID);
}
skipAnimation = true;
if (fadein) {
vid.classList.add("fadein");
if (vid.holder) {
vid.holder.classList.add("fadein");
}
}
}
offsety = Math.max((h - Math.ceil(mpl / rw) * Math.ceil(h / rh)) / 2, 0);
if (vid.container) {
var container = vid.container;
if (container.move) {
clearInterval(container.move);
container.move = null;
}
} else {
var container = document.createElement("div");
vid.container = container;
}
container.style.position = "absolute";
container.style.display = "block";
container.classList.add("container_holder_video");
// Add screen share class to individual containers
var isScreenShare = false;
if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].screenShareState) {
isScreenShare = true;
} else if (vid.id === "screensharesource") {
isScreenShare = true;
}
if (isScreenShare) {
container.classList.add("is-screenshare");
container.classList.remove("is-not-screenshare");
} else {
container.classList.add("is-not-screenshare");
container.classList.remove("is-screenshare");
}
// ANIMATED - CONTAINER ; width/height/z-index/cover///////////////
if (layout) {
try {
var left = (w / 100) * layout[vid.dataset.sid].x || layout[vid.dataset.sid].xp || 0;
var top = (h / 100) * layout[vid.dataset.sid].y || layout[vid.dataset.sid].yp || 0;
top += hi;
var width = (w / 100) * layout[vid.dataset.sid].w || layout[vid.dataset.sid].wp || 0;
var height = (h / 100) * layout[vid.dataset.sid].h || layout[vid.dataset.sid].hp || 0;
if (layout[vid.dataset.sid].cover || layout[vid.dataset.sid].c) {
// this should be true/false
//vid.style.objectFit = "cover";
cover = layout[vid.dataset.sid].cover || layout[vid.dataset.sid].c;
} else {
//vid.style.objectFit = "contain"; // this should fall back to sessio.cover if no layout supplied
cover = false;
}
//container.style.zindex = 0;
container.style.zIndex = layout[vid.dataset.sid].zIndex || layout[vid.dataset.sid].z || 0;
} catch(e){
errorlog(e);
}
} else {
var left = Math.max(offsetx + Math.floor((((i % rw) + 0) * w) / rw), 0);
var top = Math.max(offsety + Math.floor(((Math.floor(i / rw) + 0) * h) / rh + hi), 0);
var width = Math.ceil(w / rw);
var height = Math.ceil(h / rh);
//container.style.zIndex = 0;
}
var computed = getComputedStyle(vid);
if (animated && !skipAnimation) {
container.style.transition = "width " + animated + "ms ease-in-out 0s, height " + animated + "ms ease-in-out 0s, background-color " + animated + "ms ease-in-out 0s, transform " + animated + "ms ease-in-out 0s, top " + animated + "ms ease-in-out 0s, left " + animated + "ms ease-in-out 0s";
} else {
container.style.transition = "";
}
if (layout) {
////////////////// NOT ANIMATED - CONTAINER ; width/height/z-index/cover///////////////
container.style.left = left + "px";
container.style.top = top + "px";
container.style.width = width + "px";
container.style.height = height + "px";
container.twidth = width;
container.theight = height;
} else {
container.style.left = offsetx + Math.floor((((i % rw) + 0) * w) / rw) + "px";
container.style.top = offsety + Math.floor(((Math.floor(i / rw) + 0) * h) / rh + hi) + "px";
container.twidth = Math.ceil(w / rw);
container.theight = Math.ceil(h / rh);
container.style.width = container.twidth + "px";
container.style.height = container.theight + "px";
}
var maxWidth = 0;
if (parseInt(computed.width) > parseInt(container.style.width)) {
maxWidth = computed.width;
} else {
maxWidth = container.style.width;
}
var maxHeight = 0;
if (parseInt(computed.height) > parseInt(container.style.height)) {
maxHeight = computed.height;
} else {
maxHeight = container.style.height;
}
if (cover === true) {
vid.style.maxWidth = maxWidth;
vid.style.maxHeight = maxHeight;
vid.style.objectFit = "cover";
} else if (cover == 2) {
// For session.cover == 2, determine whether to use cover or contain
// based on aspect ratio comparison
vid.style.maxWidth = maxWidth;
vid.style.maxHeight = maxHeight;
const vw = vid.naturalWidth || vid.videoWidth || 0;
const vh = vid.naturalHeight || vid.videoHeight || 0;
if (vw && vh) {
// Calculate aspect ratios
const videoAspect = vw / vh;
// Use container dimensions for comparison
const containerWidth = parseFloat(maxWidth);
const containerHeight = parseFloat(maxHeight);
const containerAspect = containerWidth / containerHeight;
// If video is wider than container proportionally (width being squished),
// use cover. Otherwise use contain.
if (videoAspect > containerAspect) {
vid.style.objectFit = "cover";
} else {
vid.style.objectFit = "contain";
}
} else {
// Default to contain if we can't determine dimensions
vid.style.objectFit = "contain";
}
} else {
vid.style.objectFit = "contain";
vid.style.maxWidth = maxWidth;
vid.style.maxHeight = maxHeight;
}
//try {
if (vid.alreadyAdded && vid.alreadyAdded == true) {
if (!container.holder) {
var holder = document.createElement("div");
container.holder = holder;
holder.className = "holder";
holder.dataset.holder = true;
container.appendChild(holder);
holder.appendChild(vid);
} else {
var holder = container.holder;
}
} else if (vid.dataset.doNotMove) {
vid.style.position = "absolute";
vid.style.left = left + "px";
vid.style.top = top + "px";
vid.style.width = width + "px";
vid.style.height = height + "px";
vid.style.display = "flex";
i += 1;
return;
} else {
if (!container.holder) {
var holder = document.createElement("div");
container.holder = holder;
holder.className = "holder";
holder.dataset.holder = true;
holder.appendChild(vid);
container.appendChild(holder);
} else {
var holder = container.holder;
holder.prepend(vid);
}
playarea.appendChild(container);
vid.style.maxWidth = "100%";
vid.style.maxHeight = "100%";
}
if (layout) {
var wrw = (w / 100) * layout[vid.dataset.sid].w || 0;
var hrh = (h / 100) * layout[vid.dataset.sid].h || 0;
} else {
var wrw = w / rw;
var hrh = h / rh;
}
if (backgroundMedia) {
container.style.backgroundImage = "url(" + backgroundMedia + ")";
if (cover) {
container.style.backgroundSize = "cover";
} else {
container.style.backgroundSize = "contain";
}
container.style.backgroundPosition = "center";
container.style.backgroundRepeat = "no-repeat";
} else if (container.style.backgroundImage) {
container.style.backgroundImage = "unset";
}
if (foregroundMedia) {
if (!container.foregroundMedia) {
container.foregroundMedia = document.createElement("img");
container.foregroundMedia.className = "foregroundMedia";
container.appendChild(container.foregroundMedia);
}
container.foregroundMedia.src = foregroundMedia;
} else if (container.foregroundMedia) {
try {
container.foregroundMedia.remove();
delete container.foregroundMedia;
} catch (e) {
errorlog(e);
}
}
if (textOverlay && !container.textOverlay){
container.appendChild(textOverlay);
}
if ("rotated" in vid && vid.rotated !== false) {
if (vid.rotated == 90) {
vid.style.transform = "rotate(90deg)";
} else if (vid.rotated == 270) {
vid.style.transform = "rotate(270deg)";
} else if (vid.rotated == 180) {
vid.style.transform = "rotate(180deg)";
} else if (!vid.rotated) {
vid.style.transform = "rotate(0deg)";
}
}
vid.style.width = "100%";
vid.style.height = "100%";
holder.style.position = "absolute";
if (vid.classList.contains("paused")) {
if (holder.paused) {
holder.paused.className = "playButton";
} else {
var paused = document.createElement("span");
paused.id = "paused_" + vid.dataset.UUID;
paused.className = "playButton";
paused.dataset.UUID = vid.dataset.UUID;
paused.onclick = function () {
unPauseVideo(vid);
};
holder.paused = paused;
holder.appendChild(paused);
}
} else if (holder.paused) {
holder.paused.className = "hidden";
}
var vw = vid.naturalWidth || vid.videoWidth; // naturalWidth is for images I guess
var vh = vid.naturalHeight || vid.videoHeight;
// log(vw + " : "+vh);
if (cover && !session.structure) {
//////
if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) {
holder.style.left = borderOffset + "px";
holder.style.top = borderOffset + "px";
holder.style.height = "calc(100% - " + videoMargin * 2 + "px)";
holder.style.width = "calc(100% - " + videoMargin * 2 + "px)";
vid.style.width = height - (borderOffset + videoMargin) * 2 + "px";
vid.style.height = width - (borderOffset + videoMargin) * 2 + "px";
vid.style.left = 0;
vid.style.top = 0;
} else {
holder.style.left = videoMargin + "px";
holder.style.top = videoMargin + "px";
holder.style.height = "calc(100% - " + videoMargin * 2 + "px)";
holder.style.width = "calc(100% - " + videoMargin * 2 + "px)";
vid.style.width = "100%";
vid.style.height = "100%";
vid.style.left = 0;
vid.style.top = 0;
}
////////// COVER VERSION
if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) {
// do not dynamically scale the screen share feed.
} else if (session.dynamicScale) {
if (vid.dataset.UUID) {
let targetWidth = wrw;
let targetHeight = hrh;
targetWidth -= (borderOffset + videoMargin) * 2;
targetHeight -= (borderOffset + videoMargin) * 2;
if (targetWidth < 0) {
targetWidth = 0;
}
if (targetHeight < 0) {
targetHeight = 0;
}
if (session.devicePixelRatio) {
session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) {
session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover);
} else {
session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
}
}
}
vid.style.borderColor = borderColor;
vid.style.borderWidth = borderOffset + "px";
vid.style.borderRadius = borderRadius + "px";
holder.style.borderColor = borderColor;
holder.style.borderWidth = "0px";
holder.style.borderRadius = borderRadius + "px";
} else if ((vw && vh) || (vid.width && vid.height) || vid.dataset.aspectRatio) {
if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) {
if (vw && vh) {
var vvw = parseInt(vh);
var vvh = parseInt(vw);
} else if (vid.width && vid.height) {
var vvw = parseInt(vid.height);
var vvh = parseInt(vid.width);
} else {
// video disabled; fall back to aspect Ratio
var vvw = 1;
var vvh = vid.dataset.aspectRatio;
}
vid.style.objectFit = "cover"; //contain;
vid.style.overflow = "unset"; //contain;
//vid.style.maxWidth = "unset";
//vid.style.maxHeight = "unset";
} else {
if (vw && vh) {
var vvw = parseInt(vw);
var vvh = parseInt(vh);
} else if (vid.width && vid.height) {
var vvw = parseInt(vid.width);
var vvh = parseInt(vid.height);
} else {
var vvw = vid.dataset.aspectRatio;
var vvh = 1;
}
}
var asw = (wrw - videoMargin * 2 - borderOffset * 2) / vvw; // (window.innerWidth/ N) / vid.videoHeight;
var ash = (hrh - videoMargin * 2 - borderOffset * 2) / vvh;
if (session.structure) {
// wrw x hrh
var arx = (wrw - videoMargin * 2 - borderOffset * 2) / (hrh - videoMargin * 2 - borderOffset * 2);
var tarx = arW / arH;
//var arW = 16.0;
//var arH = 9.0;
if (arx > tarx) {
// width is too long
var hsw = hrh * tarx - videoMargin * 2 * tarx - borderOffset * 2;
var hsl = (wrw - hsw) / 2;
var hst = videoMargin;
var hsh = hrh - videoMargin * 2;
} else {
var hsh = (wrw - videoMargin * 2 + borderOffset * 2) / tarx;
var hst = (hrh - hsh) / 2;
var hsl = videoMargin;
var hsw = wrw - videoMargin * 2;
}
} else if (asw > ash) {
var hsh = hrh - videoMargin * 2;
var hst = videoMargin;
var hsw = (hsh - borderOffset * 2) * (vvw / vvh) + borderOffset * 2;
var hsl = (wrw - hsw) / 2;
} else {
var hsw = wrw - videoMargin * 2;
var hsl = videoMargin;
var hsh = (hsw - borderOffset * 2) / (vvw / vvh) + borderOffset * 2;
var hst = (hrh - hsh) / 2;
}
holder.style.left = Math.floor(hsl) + "px"; // this needs to be replaced with padding. This means testing with rotation = 90
holder.style.top = Math.floor(hst) + "px";
holder.style.width = Math.ceil(hsw) + "px";
holder.style.height = Math.ceil(hsh) + "px";
//holder.style.padding = videoMargin + "px";
holder.style.borderColor = borderColor;
holder.style.borderWidth = borderOffset + "px";
holder.style.borderRadius = borderRadius + "px";
vid.style.borderWidth = "0px";
if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) {
vid.style.width = Math.ceil(wrw - borderOffset * 2) + "px";
vid.style.height = Math.ceil(hsw - borderOffset * 2) + "px";
vid.style.left = 0;
vid.style.maxWidth = "100vh";
vid.style.maxHeight = "100vw";
if (ChromiumVersion && ChromiumVersion < 77) {
if (!animated && parseInt(container.style.width) > parseInt(holder.style.height)) {
vid.style.position = "relative";
vid.style.objectFit = "contain"; //contain;
} else if (animated && container.twidth && parseInt(container.twidth) > parseInt(holder.style.height)) {
vid.style.position = "relative";
vid.style.objectFit = "contain"; //contain;
}
} else {
vid.style.position = "relative";
}
} else if (session.blurBackground !== false && vid.nodeName == "VIDEO" && !container.blurred && vid.srcObject && ((asw > 1 && ash > 1) || asw >= 1 || ash >= 1)) {
vid.srcObject.getVideoTracks().forEach(trk => {
if (!container.blurred) {
container.blurred = document.createElement("video");
container.blurred.controls = false;
container.blurred.style = "z-index:-10000;position:absolute;left:0;width:100%;height:100%;top:0;object-fit:fill;-webkit-filter: blur(" + session.blurBackground + "px)";
container.blurred.srcObject = createMediaStream();
container.blurred.srcObject.addTrack(trk);
container.blurred.play();
holder.appendChild(container.blurred);
} else {
if (container.blurred.paused) {
container.blurred.play();
}
}
});
} else if (session.blurBackground !== false && vid.nodeName == "VIDEO" && vid.srcObject && ((asw > 1 && ash > 1) || asw >= 1 || ash >= 1)) {
if (container.blurred.paused) {
container.blurred.play();
}
//container.blurred.style = "z-index:-10000;position:absolute;left:0;width:100%;height:100%;top:0;object-fit:fill;-webkit-filter: blur("+session.blurBackground+"px)";
} else if (container.blurred) {
try {
container.blurred.remove();
} catch (e) {}
try {
delete container.blurred;
} catch (e) {}
}
////////// NON-COVER VERSION (based on holder)
if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) {
// do not dynamically scale the screen share feed.
} else if (session.dynamicScale) {
if (vid.dataset.UUID) {
let targetWidth = wrw;
let targetHeight = hrh;
targetWidth -= (borderOffset + videoMargin) * 2;
targetHeight -= (borderOffset + videoMargin) * 2;
if (targetWidth < 0) {
targetWidth = 0;
}
if (targetHeight < 0) {
targetHeight = 0;
}
if (session.devicePixelRatio) {
session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) {
session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover);
} else {
session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
}
}
}
} else {
holder.style.left = borderOffset + videoMargin + "px";
holder.style.top = borderOffset + videoMargin + "px";
holder.style.height = "calc(100% - " + (borderOffset + videoMargin * 2) + "px)";
holder.style.width = "calc(100% - " + (borderOffset + videoMargin * 2) + "px)";
////////// UNKNOWN VERSION
if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) {
// do not dynamically scale the screen share feed.
} else if (session.dynamicScale) {
if (vid.dataset.UUID) {
let targetWidth = wrw;
let targetHeight = hrh;
targetWidth -= (borderOffset + videoMargin) * 2;
targetHeight -= (borderOffset + videoMargin) * 2;
if (targetWidth < 0) {
targetWidth = 0;
}
if (targetHeight < 0) {
targetHeight = 0;
}
if (session.devicePixelRatio) {
session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) {
session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover);
} else {
session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
}
}
}
///////////////
holder.style.borderColor = borderColor;
holder.style.borderWidth = borderOffset + "px";
holder.style.borderRadius = borderRadius + "px";
vid.style.borderWidth = "0px";
}
if (session.colorVideosBackground) {
vid.style.backgroundColor = session.colorVideosBackground;
} else {
vid.style.backgroundColor = "unset";
}
if ((vid.dataset.UUID && session.rpcs && session.rpcs[vid.dataset.UUID] &&
(
("label" in session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].label !== false && session.showlabels === true) ||
(session.showmeta === true && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].meta)
))
||
((session.showlabels === true || session.showmeta === true) &&
(((vid.id === "videosource") && session.label) || vid.labelText || session.meta)
)) {
if (animated && container.twidth && container.theight) {
var vidwidth = container.twidth;
var vidheight = container.theight;
} else {
var vidwidth = vid.offsetWidth;
var vidheight = vid.offsetHeight;
}
var fontsize = (vidwidth + vidheight) * 0.03;
if (vidwidth / 16 >= vidheight / 9) {
var voar = vidwidth / 16 / (vidheight / 9);
} else {
var voar = vidheight / 9 / (vidwidth / 16);
}
voar = Math.pow(voar, 0.5);
fontsize = fontsize / voar;
if (holder.label) {
var label = holder.label;
} else {
var label = document.createElement("span");
holder.label = label;
if (session.labelstyle) {
label.className = "video-label " + session.labelstyle;
} else {
label.className = "video-label";
}
holder.appendChild(label);
}
if (fontsize) {
if (session.labelsize) {
fontsize = (fontsize * session.labelsize) / 100;
}
label.style.fontSize = parseInt(fontsize) + "px";
}
if ( (
vid.dataset.UUID &&
session.rpcs &&
session.rpcs[vid.dataset.UUID] &&
(
// Label check
(
"label" in session.rpcs[vid.dataset.UUID] &&
session.rpcs[vid.dataset.UUID].label !== false &&
session.showlabels === true
)
||
// Meta check with explicit undefined checks
(
session.showmeta === true &&
session.rpcs[vid.dataset.UUID] &&
session.rpcs[vid.dataset.UUID].meta
)
)
)
||
(
(session.showlabels === true || session.showmeta === true) &&
(
((vid.id === "videosource") && session.label) ||
vid.labelText ||
session.meta
)
)
) {
let labelContent = [];
let imageElements = [];
if (session.showlabels === true) {
if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].label) {
session.rpcs[vid.dataset.UUID].label.split("\\n").forEach(label => {
labelContent.push({type: 'text', value: label});
});
} else if ((vid.id === "videosource") && session.label || vid.labelText) {
(vid.labelText || session.label).split("\\n").forEach(label => {
labelContent.push({type: 'text', value: label});
});
}
}
if (session.showmeta === true) {
let metaData = [];
if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].meta) {
metaData = Object.entries(session.rpcs[vid.dataset.UUID].meta);
} else if (session.meta) {
metaData = Object.entries(session.meta);
}
metaData.forEach(([key, value]) => {
if (value && typeof value === 'object') {
if (value.type && value.type == 'file') {
// console.log(value);
if (value.filetype){
imageElements.push(value);
}
} else {
labelContent.push({
type: 'text',
value: value.value || JSON.stringify(value)
});
}
} else if (value) {
labelContent.push({type: 'text', value: value});
}
});
}
// Create or get label container
if (!holder.labelContainer) {
holder.labelContainer = document.createElement("div");
holder.labelContainer.className = "video-label-container";
holder.appendChild(holder.labelContainer);
}
// Handle text content
if (labelContent.length > 0) {
if (!holder.label) {
holder.label = document.createElement("div");
if (session.labelstyle) {
holder.label.className = "video-label " + session.labelstyle;
} else {
holder.label.className = "video-label";
}
holder.labelContainer.appendChild(holder.label);
}
// Apply font size adjustments as before
if (fontsize) {
if (session.labelsize) {
fontsize = (fontsize * session.labelsize) / 100;
}
holder.label.style.fontSize = parseInt(fontsize) + "px";
}
holder.label.innerHTML = labelContent
.map(item => `${escapeHtml(String(item.value))}`)
.join("");
}
// Handle image content
if (imageElements.length > 0) {
if (!holder.imageContainer) {
holder.imageContainer = document.createElement("div");
holder.imageContainer.className = "video-image-container";
holder.imageContainer.style.position = "absolute";
holder.imageContainer.style.top = "10px";
holder.imageContainer.style.right = "10px";
holder.imageContainer.style.display = "flex";
holder.imageContainer.style.alignItems = "flex-start";
holder.imageContainer.style.justifyContent = "flex-end";
holder.appendChild(holder.imageContainer);
}
holder.imageContainer.style.maxWidth = "min(20vw, calc("+holder.style.width+" * 0.25))";
holder.imageContainer.style.maxHeight = "min(20vh, calc("+holder.style.height+" * 0.25))";
holder.imageContainer.style.minWidth = "max(7vw, calc("+holder.style.width+" * 0.22), 30px)";
holder.imageContainer.style.minHeight = "max(7vw, calc("+holder.style.height+" * 0.22), 30px)";
// Clear existing images
holder.imageContainer.innerHTML = '';
// Add new images
imageElements.forEach(meta => {
if (meta.filetype.startsWith("image/")){
const imgElement = document.createElement("img");
imgElement.src = meta.value;
imgElement.className = `meta-image ${meta.templateName || ''}`;
imgElement.style.width = "auto"; // Fill container width
imgElement.style.height = "auto"; // Fill container height
imgElement.style.objectFit = "contain"; // Maintain aspect ratio
imgElement.style.position = "absolute";
imgElement.style.maxWidth = "100%";
imgElement.style.maxHeight = "100%";
holder.imageContainer.appendChild(imgElement);
}
});
}
}
} else if (holder.label) {
holder.label.remove();
delete holder.label;
}
if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID]) {
if (session.rpcs[vid.dataset.UUID].voiceMeter) {
holder.appendChild(session.rpcs[vid.dataset.UUID].voiceMeter);
}
if (session.rpcs[vid.dataset.UUID].remoteMuteElement) {
holder.appendChild(session.rpcs[vid.dataset.UUID].remoteMuteElement);
}
if (session.signalMeter) {
if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].signalMeter) {
session.rpcs[vid.dataset.UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true);
session.rpcs[vid.dataset.UUID].signalMeter.classList.remove("hidden");
session.rpcs[vid.dataset.UUID].signalMeter.id = "signalMeter_" + vid.dataset.UUID;
session.rpcs[vid.dataset.UUID].signalMeter.dataset.level = 0;
session.rpcs[vid.dataset.UUID].signalMeter.title = getTranslation("signal-meter");
holder.appendChild(session.rpcs[vid.dataset.UUID].signalMeter);
holder.signalMeter = session.rpcs[vid.dataset.UUID].signalMeter;
} else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].signalMeter) {
if (!holder.signalMeter) {
holder.appendChild(session.rpcs[vid.dataset.UUID].signalMeter);
holder.signalMeter = session.rpcs[vid.dataset.UUID].signalMeter;
}
}
}
if (session.batteryMeter) {
if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].batteryMeter) {
session.rpcs[vid.dataset.UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true);
session.rpcs[vid.dataset.UUID].batteryMeter.classList.remove("hidden");
session.rpcs[vid.dataset.UUID].batteryMeter.id = "batteryMeter_" + vid.dataset.UUID;
session.rpcs[vid.dataset.UUID].batteryMeter.dataset.level = 0;
session.rpcs[vid.dataset.UUID].batteryMeter.title = getTranslation("battery-meter");
holder.appendChild(session.rpcs[vid.dataset.UUID].batteryMeter);
holder.batteryMeter = session.rpcs[vid.dataset.UUID].batteryMeter;
} else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].batteryMeter) {
if (!holder.batteryMeter) {
holder.appendChild(session.rpcs[vid.dataset.UUID].batteryMeter);
holder.batteryMeter = session.rpcs[vid.dataset.UUID].batteryMeter;
}
}
}
if (session.volumeControl && session.rpcs[vid.dataset.UUID].videoElement && vid.tagName != "VIDEO") {
if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].volumeControl) {
session.rpcs[vid.dataset.UUID].volumeControl = getById("volumeControlTemplate").cloneNode(true);
session.rpcs[vid.dataset.UUID].volumeControl.classList.remove("hidden");
session.rpcs[vid.dataset.UUID].volumeControl.id = "volumeControl_" + vid.dataset.UUID;
session.rpcs[vid.dataset.UUID].volumeControl.value = parseInt(session.rpcs[vid.dataset.UUID].videoElement.volume * 100);
session.rpcs[vid.dataset.UUID].volumeControl.dataset.UUID = vid.dataset.UUID;
session.rpcs[vid.dataset.UUID].volumeControl.title = getTranslation("volume-control");
session.rpcs[vid.dataset.UUID].volumeControl.oninput = function () {
if (this.dataset.UUID && session.rpcs[this.dataset.UUID] && session.rpcs[this.dataset.UUID].videoElement) {
session.rpcs[this.dataset.UUID].videoElement.volume = parseFloat(this.value / 100);
}
};
holder.appendChild(session.rpcs[vid.dataset.UUID].volumeControl);
holder.volumeControl = session.rpcs[vid.dataset.UUID].volumeControl;
} else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].volumeControl) {
if (!holder.volumeControl && session.rpcs[vid.dataset.UUID].videoElement) {
holder.appendChild(session.rpcs[vid.dataset.UUID].volumeControl);
holder.volumeControl = session.rpcs[vid.dataset.UUID].volumeControl;
session.rpcs[vid.dataset.UUID].volumeControl.value = parseInt(session.rpcs[vid.dataset.UUID].videoElement.volume * 100);
}
}
}
if (session.showConnections) {
if (!session.rpcs[vid.dataset.UUID].connectionDetails) {
createConnectionDetailsEle(vid.dataset.UUID);
}
holder.appendChild(session.rpcs[vid.dataset.UUID].connectionDetails);
}
}
if (session.ruleOfThirds) {
if (vid.id == "videosource") {
if (!holder.svg) {
var svg = document.createElement("img");
svg.src = session.ruleOfThirds;
svg.style.width = "100%";
svg.style.height = "100%";
svg.style.position = "absolute";
svg.style.left = "0";
svg.style.top = "0";
svg.style.pointerEvents = "none";
holder.svg = svg;
holder.appendChild(svg);
}
}
}
try {
if (!(session.cleanOutput && session.cleanish == false)) {
if (session.firstPlayTriggered === false) {
// don't play unless needed; might cause clicking or who knows what else.
if (vid.tagName.toLowerCase() == "video") {
// we don't want to try playing an Iframe or Canvas.
if (vid.paused) {
warnlog("VIDEO IS NOT PLAYING");
var playPromise = vid.play();
if (playPromise !== undefined) {
playPromise
.then(_ => {
// playing
//session.firstPlayTriggered=true; // global tracking. "user gesture obtained", so no longer needed if playing.
})
.catch(err => {
var bigPlayButton = document.getElementById("bigPlayButton");
if (bigPlayButton) {
bigPlayButton.innerHTML = '';
bigPlayButton.style.display = "block";
}
});
} else {
session.firstPlayTriggered = true; // well, I don't know if it's playing, and so whatever. fail gracefully.
}
}
}
}
}
} catch (e) {
errorlog(e);
var bigPlayButton = document.getElementById("bigPlayButton");
if (bigPlayButton) {
bigPlayButton.parentNode.removeChild(bigPlayButton);
}
}
if (vid.nodeName == "IFRAME") {
// I need to add this back in at some point.
i += 1;
return;
}
if (!session.cleanOutput && !session.nocursor && !session.nofullwindowbutton) {
if (session.roomid !== false && session.scene === false) {
if (!(vid.id === "videosource" && miniPreview) && !session.infocusForceMode) {
if (!holder.button) {
var button = document.createElement("div");
holder.button = button;
holder.appendChild(button);
} else {
var button = holder.button;
}
button.id = "button_" + vid.id;
button.dataset.button = true;
if (soloVideo) {
button.innerHTML = "";
button.title = "Show all active videos togethers";
button.style.visibility = "visible";
} else if (mpl > 1 || session.fullscreenButton) {
// with session.fullscreenButton we hide the actuall full screen button, so this replaces it
button.innerHTML = "";
button.title = "Enlarge video and increase its clarity";
button.style.visibility = "visible";
} else {
button.style.visibility = "hidden";
}
button.classList.add("fullwindowButton");
if (vid.id == "videosource") {
button.onclick = function (event) {
if (session.infocus === true) {
session.infocus = false;
} else {
session.infocus = true;
}
setTimeout(() => updateMixer(), 10);
if (session.fullscreenButton) {
if (session.infocus) {
fullscreenPageToggle(true);
} else {
fullscreenPageToggle(false);
}
}
};
} else {
button.dataset.UUID = vid.dataset.UUID;
button.onclick = function (event) {
var target = event.currentTarget;
if (session.infocus === target.dataset.UUID) {
//target.childNodes[0].className = 'las la-arrows-alt';
session.infocus = false;
} else {
//target.childNodes[0].className = 'las la-compress';
session.infocus = target.dataset.UUID;
//log("session:"+target.dataset.UUID);
}
if (session.fullscreenButton) {
if (session.infocus) {
fullscreenPageToggle(true);
} else {
fullscreenPageToggle(false);
}
}
setTimeout(() => updateMixer(), 10);
};
}
vid.onclick = function (event) {
if (session.disableMouseEvents) {
return;
}
button.style.display = "block";
container.style.backgroundColor = "#4444";
button.style.opacity = "100%";
};
button.onmouseenter = function (event) {
if (session.disableMouseEvents) {
return;
}
button.style.display = "block";
container.style.backgroundColor = "#4444";
setTimeout(
function (button) {
button.style.opacity = "100%";
},
1,
button
);
};
button.onmousemove = function (event) {
if (session.disableMouseEvents) {
return;
}
button.style.display = "block";
container.style.backgroundColor = "#4444";
button.style.opacity = "100%";
};
container.onmousemove = function (event) {
if (session.disableMouseEvents) {
return;
}
button.style.display = "block";
container.style.backgroundColor = "#4444";
button.style.opacity = "100%";
};
container.onmouseenter = function (event) {
if (session.disableMouseEvents) {
return;
}
button.style.display = "block";
container.style.backgroundColor = "#4444";
setTimeout(
function (button) {
button.style.opacity = "100%";
},
1,
button
);
};
container.onmouseleave = function (event) {
if (session.disableMouseEvents) {
return;
}
button.style.display = "none";
container.style.backgroundColor = null;
button.style.opacity = "10%";
};
} else if (vid.id === "videosource" && miniPreview && soloVideo == true && !session.infocusForceMode) {
if (!holder.button) {
var button = document.createElement("div");
holder.button = button;
holder.appendChild(button);
} else {
var button = holder.button;
}
button.id = "button_videosource";
button.dataset.button = true;
if (soloVideo) {
button.innerHTML = "";
button.title = "Show all active videos togethers";
button.style.display = "unset";
} else {
button.style.visibility = "hidden";
button.style.display = "none";
}
button.classList.add("fullwindowButton");
button.onclick = function (event) {
event.stopPropagation();
event.preventDefault();
if (session.infocus === true) {
session.infocus = false;
setTimeout(() => updateMixer(), 10);
}
if (session.fullscreenButton) {
if (session.infocus) {
fullscreenPageToggle(true);
} else {
fullscreenPageToggle(false);
}
}
};
} else if (session.infocusForceMode && holder.button) {
try {
holder.button.remove();
} catch (e) {
errorlog(e);
}
}
}
}
i += 1;
} catch (err) {
errorlog(err);
}
});
for (var uid in delayedRequestList) {
session.requestRateLimit(...delayedRequestList[uid]);
}
updateUserList();
}
var translationBacklog = [];
function miniTranslate(ele, ident = false, direct = false) {
if (!translation) {
translation = {};
}
if (ident) {
if (translation.innerHTML && ident in translation.innerHTML) {
if (ele.querySelector("[data-translate]")) {
ele.querySelector("[data-translate]").innerHTML = translation.innerHTML[ident];
ele.querySelector("[data-translate]").dataset.translate = ident;
} else {
ele.innerHTML = translation.innerHTML[ident];
ele.dataset.translate = ident;
}
return;
} else if (direct) {
if (ele.querySelector("[data-translate]")) {
ele.querySelector("[data-translate]").innerHTML = direct;
ele.querySelector("[data-translate]").dataset.translate = ident;
} else {
ele.dataset.translate = ident;
ele.innerHTML = direct;
}
return;
} else {
if (!(ident in miscTranslations)) {
var value = ident.replaceAll("-", " "); // lets use the key as the translation
} else {
var value = miscTranslations[ident]; // lets use a miscellaneous translation as backup?
}
if (ele.querySelector("[data-translate]")) {
ele.querySelector("[data-translate]").innerHTML = value;
ele.querySelector("[data-translate]").dataset.translate = ident;
} else {
ele.innerHTML = value;
ele.dataset.translate = ident;
}
return;
}
}
var allItems = ele.querySelectorAll("[data-translate]");
allItems.forEach(function (ele2) {
if (translation.innerHTML && ele2.dataset.translate in translation.innerHTML) {
ele2.innerHTML = translation.innerHTML[ele2.dataset.translate];
} else if (translation.miscellaneous && ele2.dataset.translate in translation.miscellaneous) {
ele2.innerHTML = translation.miscellaneous[ele2.dataset.translate];
}
});
if (ele.dataset) {
if (translation.innerHTML && ele.dataset.translate in translation.innerHTML) {
ele.innerHTML = translation.innerHTML[ele.dataset.translate];
} else if (translation.miscellaneous && ele.dataset.translate in translation.miscellaneous) {
ele.innerHTML = translation.miscellaneous[ele.dataset.translate];
}
}
if (translation.titles) {
var allTitles = ele.querySelectorAll("[title]");
allTitles.forEach(function (ele2) {
if (ele.dataset.key) {
var key = ele2.dataset.key;
} else {
var key = ele2.title.replace(/[\W]+/g, "-").toLowerCase();
ele2.dataset.key = key;
}
if (key in translation.titles) {
ele2.title = translation.titles[key];
} else if (ele2.dataset.translate && ele2.dataset.translate in translation.titles) {
ele2.title = translation.titles[ele2.dataset.translate];
}
});
if (ele.title) {
if (ele.dataset.key) {
var key = ele.dataset.key;
} else {
var key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
ele.dataset.key = key;
}
if (key in translation.titles) {
ele.title = translation.titles[key];
} else if (ele.dataset.translate && ele.dataset.translate in translation.titles) {
ele.title = translation.titles[ele.dataset.translate];
}
}
}
if (translation.placeholders) {
var allPlaceholders = ele.querySelectorAll("[placeholder]");
allPlaceholders.forEach(function (ele2) {
var key = ele2.placeholder.replace(/[\W]+/g, "-").toLowerCase();
if (key in translation.placeholders) {
ele2.placeholder = translation.placeholders[key];
}
});
if (ele.placeholder) {
var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
if (key in translation.placeholders) {
ele.placeholder = translation.placeholders[key];
}
}
}
}
async function changeLg(lang, rtl=false, save=false) {
log("changeLg: " + lang);
var retry = false;
if (lang == "auto") {
const navlang = navigator.language || navigator.userLanguage || "";
const userLang = navlang.toLowerCase(); // Convert to lower case
lang = userLang.replace("_", "-"); // Replace dash with underscore if present
retry = true;
}
await fetchWithTimeout("./translations/" + lang + ".json", 2000)
.then(async response => {
try {
if (response.status !== 200) {
getById("mainmenu").style.opacity = 1;
if (retry) {
console.warn("Couldn't find the exact language file for '" + lang + "'; trying a more generic option instead");
lang = lang.split("-")[0];
if (lang && lang !== "auto") {
await changeLg(lang, rtl); // Retry with a more generic language code.
}
}
return;
}
await response
.json()
.then(async function (data) {
translation = data; // translation.innerHTML[ele.dataset.translate]
document.documentElement.dir = rtl ? "rtl" : "ltr";
document.documentElement.setAttribute('lang', lang);
document.body.classList.remove('rtl');
document.body.classList.remove('ltr');
document.body.classList.add(rtl ? 'rtl' : 'ltr');
document.documentElement.style.setProperty('--rtl-or-ltr', rtl ? 'right' : 'left');
if (save){
try {
localStorage.setItem("vdo_ninja_language", lang);
} catch (e) {
console.warn("Could not save language to localStorage", e);
}
}
if (translation.miscellaneous) {
Object.keys(translation.miscellaneous).forEach(key => {
miscTranslations[key] = translation.miscellaneous[key];
});
}
translation.miscellaneous = miscTranslations;
var allItems = document.querySelectorAll("[data-translate]");
allItems.forEach(function (ele) {
if (ele.dataset.translate in translation.innerHTML) {
ele.innerHTML = translation.innerHTML[ele.dataset.translate];
} else if (translation.miscellaneous && ele.dataset.translate in translation.miscellaneous) {
ele.innerHTML = translation.miscellaneous[ele.dataset.translate]; // use the misc translation if no main one is found
}
});
var allTitles = document.querySelectorAll("[title]");
allTitles.forEach(function (ele) {
if (ele.dataset.key) {
var key = ele.dataset.key;
} else {
var key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
ele.dataset.key = key;
}
if (key in translation.titles) {
ele.title = translation.titles[key];
} else if (ele.dataset.translate && translation.titles && ele.dataset.translate in translation.titles) {
ele.title = translation.titles[ele.dataset.translate];
}
});
var allPlaceholders = document.querySelectorAll("[placeholder]");
allPlaceholders.forEach(function (ele) {
var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
if (key in translation.placeholders) {
ele.placeholder = translation.placeholders[key];
}
});
if (translationBacklog.length) {
for (var i = 0; i < translationBacklog.length; i++) {
try {
miniTranslate(translationBacklog[i][0], translationBacklog[i][1]);
} catch (e) {}
}
translationBacklog = [];
}
getById("mainmenu").style.opacity = 1;
})
.catch(async err2 => {
if (retry) {
console.warn("Couldn't find the exact language file for '" + lang + "'; trying a more generic option instead");
lang = lang.split("-")[0];
if (lang && lang !== "auto") {
await changeLg(lang, rtl); // won't retry I'd hope.
}
} else {
errorlog(err2);
}
});
} catch (e) {
getById("mainmenu").style.opacity = 1;
}
})
.catch(async err => {
if (retry) {
console.warn("Couldn't find exact language; trying a more generic option instead");
lang = lang.split("-")[0];
if (lang && lang !== "auto") {
await changeLg(lang, rtl); // won't retry I'd hope.
}
} else {
errorlog(err);
}
});
}
var controlBarTimeout = false;
function showControl(e) {
if (controlBarTimeout) {
clearTimeout(controlBarTimeout);
}
if (session.mobile) {
getById("controlButtons").classList.remove("partialFadeout");
} else {
getById("controlButtons").classList.remove("fadeout");
}
controlBarTimeout = setTimeout(function () {
if (session.mobile) {
getById("controlButtons").classList.add("partialFadeout");
} else {
getById("controlButtons").classList.add("fadeout");
}
}, 5000);
}
var loadedQRCode = false;
function loadQR(callback = false, value = false) {
if (loadedQRCode === false) {
loadedQRCode = true;
var script = document.createElement("script");
if (callback) {
script.onload = function () {
callback(value);
};
}
script.src = "./thirdparty/qrcode.min.js"; // dynamically load this only if its needed. Keeps loading time down.
document.head.appendChild(script);
} else if (callback) {
callback(value);
}
}
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
// this listens for child IFRAMES.
try {
if (e.origin == "https://www.youtube.com") {
processYoutubeEvent(e);
} else if (e.data && typeof e.data == "object" && "action" in e.data) {
if (e.data.action == "screen-share-state" && !e.data.value) {
//pokeIframeAPI("screen-share-state", false);
if (session.screenShareElement && session.screenShareElement.contentWindow) {
if (e.source == session.screenShareElement.contentWindow) {
// reject messages send from other iframes
warnlog(e);
postMessageIframe(session.screenShareElement, { close: true });
session.screenShareElement.parentNode.removeChild(session.screenShareElement);
session.screenShareElement = false;
updateMixer();
}
}
} else if (e.data.action == "video-loaded") {
// TODO: if (e.source == session...iframeEle.contentWindow) {
warnlog(e);
toggleSpeakerMute(true);
updateMixer(); // harmless to let run. (not so harmless if updateMixer reloads meshcast actually) TODO; Do I need this?
}
}
} catch (e) {
errorlog(e);
}
});
function requestRotateGuest(ele) {
var UUID = ele.dataset.UUID;
var data = {};
//data.mirrorGuestTarget = UUID;
//session.sendPeers(data, false, UUID);
ele.classList.add("pressed");
ele.ariaPressed = "true";
data.rotate = true;
session.sendRequest(data, UUID);
setTimeout(
function (el) {
el.value = 0;
el.classList.remove("pressed");
el.ariaPressed = "false";
},
500,
ele
);
}
function requestMirrorGuest(ele) {
var UUID = ele.dataset.UUID;
if (ele.value == 1) {
ele.value = 0;
ele.classList.remove("pressed");
ele.ariaPressed = "false";
applyMirrorGuest(false, session.rpcs[UUID].videoElement);
var data = {};
data.mirrorGuestState = false;
data.mirrorGuestTarget = UUID;
session.sendPeers(data, false, UUID);
data.mirrorGuestTarget = true;
session.sendPeers(data, UUID);
} else {
ele.value = 1;
ele.classList.add("pressed");
ele.ariaPressed = "true";
applyMirrorGuest(true, session.rpcs[UUID].videoElement);
var data = {};
data.mirrorGuestState = true;
data.mirrorGuestTarget = UUID;
session.sendPeers(data, false, UUID);
data.mirrorGuestTarget = true;
session.sendPeers(data, UUID);
}
}
function requestKeyframeScene(ele) {
var UUID = ele.dataset.UUID;
if (ele.value == 1) {
} else {
ele.value = 1;
ele.classList.add("pressed");
ele.ariaPressed = "true";
session.requestKeyframe(UUID, true);
setTimeout(
function (el) {
el.value = 0;
el.classList.remove("pressed");
el.ariaPressed = "false";
},
1000,
ele
);
}
}
function pokeIframeAPI(action, value = null, UUID = null, SID = null, CID = null) {
if (!isIFrame) {
return;
}
try {
var data = {};
data.action = action;
if (value !== null) {
data.value = value;
}
if (UUID !== null) {
data.UUID = UUID;
}
if (!SID) {
if (UUID && UUID in session.rpcs) {
if (session.rpcs[UUID].streamID) {
SID = session.rpcs[UUID].streamID;
}
}
}
if (!SID) {
if (UUID && UUID in session.pcs) {
if (session.pcs[UUID].streamID) {
SID = session.pcs[UUID].streamID;
}
}
}
if (SID) {
data.streamID = SID;
}
if (CID) {
data.cib = CID;
}
if (isIFrame) {
parent.postMessage(data, session.iframetarget);
}
} catch (e) {
errorlog(e);
}
}
async function jumptoroom2() {
var arr = window.location.href.split("?");
var roomname = getById("videoname1").value;
roomname = sanitizeRoomName(roomname);
if (roomname.length) {
var pass = getById("passwordRoom").value;
pass = sanitizePassword(pass);
var passStr = "";
if (pass && pass.length) {
passStr = "&password=" + pass;
}
if (arr.length > 1 && arr[1] !== "") {
window.location += "&room=" + roomname + passStr + "&host";
} else {
window.location += "?room=" + roomname + passStr + "&host";
}
} else {
getById("videoname1").focus();
getById("videoname1").classList.remove("shake");
setTimeout(function () {
getById("videoname1").classList.add("shake");
}, 10);
}
}
async function jumptoroom(event = null) {
if (event) {
if (event.which !== 13) {
return;
}
}
var arr = window.location.href.split("?");
var roomname = getById("joinroomID").value;
roomname = sanitizeRoomName(roomname);
if (roomname.length) {
var passStr = "";
window.focus();
var pass = await promptAlt(getTranslation("enter-password-if-desired"), false, true); //sanitizePassword(session.password);
if (pass && pass.length) {
session.password = sanitizePassword(pass);
passStr = "&password=" + session.password;
} else {
session.password = false;
}
if (arr.length > 1 && arr[1] !== "") {
window.location += "&room=" + roomname + passStr;
} else {
window.location += "?room=" + roomname + passStr;
}
} else {
getById("joinroomID").focus();
getById("joinroomID").classList.remove("shake");
setTimeout(function () {
getById("joinroomID").classList.add("shake");
}, 10);
}
}
async function jumptoURL(event = null) {
// this is for the native app
var url = getById("joinbyURL").value;
if (url.length) {
if (url.startsWith("?")) {
url = "./" + url;
}
if (url.startsWith("&")) {
url = "./?" + url;
}
if (!url.startsWith("http") && !url.startsWith(".")) {
url = "./?" + url;
}
setStorage("jumptoURL", url, 1008); // should be really only used by the native app; 6 months
window.location = url;
} else {
getById("joinbyURL").focus();
getById("joinbyURL").classList.remove("shake");
setTimeout(function () {
getById("joinbyURL").classList.add("shake");
}, 10);
}
}
function sleep(ms = 0) {
return new Promise(r => setTimeout(r, ms)); // LOLz!
}
function sleepCancellable(ms = 0) {
let resolve;
const promise = new Promise(r => {
resolve = r;
setTimeout(resolve, ms);
});
return { promise, resolve };
}
async function changeAvatarImage(ev, ele, set = false) {
log("changeAvatarImage() triggered");
if (session.avatar && session.avatar.timer) {
clearInterval(session.avatar.timer);
session.avatar.timer = null;
}
if (!session.streamSrc) {
checkBasicStreamsExist();
}
if (ele.files && ele.files.length) {
session.avatar = document.querySelector("img");
session.avatar.ready = false;
session.avatar.onload = () => {
URL.revokeObjectURL(session.avatar.src); // no longer needed, free memory
session.avatar.ready = true;
getById("noAvatarSelected3").classList.remove("selected");
getById("noAvatarSelected").classList.remove("selected");
getById("defaultAvatar1").classList.add("selected");
getById("defaultAvatar2").classList.add("selected");
var tracks = session.streamSrc.getVideoTracks();
if (!tracks.length || session.videoMuted) {
updateRenderOutpipe();
}
};
session.avatar.src = URL.createObjectURL(ele.files[0]); // set src to blob url
return;
} else if (ele.tagName.toLowerCase() == "img") {
session.avatar = ele;
session.avatar.ready = true;
getById("noAvatarSelected3").classList.remove("selected");
getById("noAvatarSelected").classList.remove("selected");
getById("defaultAvatar1").classList.add("selected");
getById("defaultAvatar2").classList.add("selected");
var tracks = session.streamSrc.getVideoTracks();
if (!tracks.length || session.videoMuted) {
updateRenderOutpipe();
}
} else {
session.avatar = false;
var tracks = session.streamSrc.getVideoTracks();
if (!tracks.length || session.videoMuted) {
var msg = {};
msg.videoMuted = true;
session.sendMessage(msg);
if (document.getElementById("videosource")) {
document.getElementById("videosource").load();
} else if (document.getElementById("previewWebcam")) {
document.getElementById("previewWebcam").load();
}
updateRenderOutpipe();
}
getById("noAvatarSelected3").classList.add("selected");
getById("noAvatarSelected").classList.add("selected");
getById("defaultAvatar1").classList.remove("selected");
getById("defaultAvatar2").classList.remove("selected");
return;
}
}
session.autoSyncCallback = function (UUID = null) {
// session.autoSyncObject has been updated. You can overwrite this with your own function
log(session.autoSyncObject);
pokeIframeAPI("auto-sync-updated", session.autoSyncObject, UUID);
};
session.autoSync = function (alternative = null) {
// Update session.autoSyncObject and then run session.autoSync()
var msg = {};
if (alternative === null) {
msg.autoSync = session.autoSyncObject;
} else {
session.autoSyncObject = alternative;
msg.autoSync = session.autoSyncObject;
}
session.sendPeers(msg);
};
//function updateRemotePTZControls(videoOptions, UUID){
// console.log(videoOptions);
// console.log(UUID);
//}
function isolateIncomingChannel(channel, UUID) {
if (!session.rpcs[UUID]) return;
if (channel === 0 || channel === false) {
delete session.rpcs[UUID].isolatedChannel;
} else {
session.rpcs[UUID].isolatedChannel = channel || session.rpcs[UUID].isolatedChannel;
}
updateIncomingAudioElement(UUID);
}
function isolateChannel(source, channel) {
if (!channel || channel === 0) {
return source; // No isolation, return the original source
}
const splitter = session.audioCtx.createChannelSplitter(6); // Assuming max 6 channels
const merger = session.audioCtx.createChannelMerger(1); // Mono output
source.connect(splitter);
splitter.connect(merger, channel - 1, 0); // Connect the specified channel to the mono output
return merger;
}
function directIsolateChannel(UUID, channel=null){ // isolateChannel()
try {
if (UUID){
var targets = document.querySelectorAll("[data--u-u-i-d='" + UUID + "'][data-action-type='isolate-channel']");
var add = false;
if (channel){
add = true;
}
targets.forEach(ele=>{
if (channel && parseInt(ele.dataset.channel) && (parseInt(ele.dataset.channel) == channel)){
if (ele.classList.contains("pressed")){
add = false;
ele.classList.remove("pressed");
ele.ariaPressed = "false";
} else {
ele.classList.add("pressed");
ele.ariaPressed = "true";
}
} else {
ele.classList.remove("pressed");
ele.ariaPressed = "false";
}
});
var msg = {};
if (add){
msg.isolateChannel = channel
} else {
msg.isolateChannel = false;
}
session.sendMessage(msg, UUID);
}
} catch (e) {
errorlog(e);
}
}
function uploadImageSnapshot(PostURL) {
if (!session.videoElement) {
return;
}
const video = session.videoElement;
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoHeight); // for drawing the video element on the canvas
const playImage = new Image();
canvas.getContext("2d").drawImage(playImage, 0, 0, playImage.width, playImage.height);
canvas.toBlob(
function (blob) {
var request = new XMLHttpRequest();
if (PostURL.includes("?")) {
request.open("POST", PostURL + "&name=" + session.streamID);
} else {
request.open("POST", PostURL + "?name=" + session.streamID);
}
request.setRequestHeader("Content-Type", "image/jpeg");
request.send(blob);
log("Posted image");
},
"image/jpeg",
0.9
);
}
var slideshowImage = false;
var slideshowCanvas = false;
var slideshowActive = false;
function slideshowHack() {
// just shows an animated IMAGE of the first video. audio plays, but no "video"
if (!session.manual) {
session.manual = session.manual === null ? true : session.manual;
}
if (slideshowActive) {
return;
}
slideshowActive = true;
try {
if (!slideshowImage) {
slideshowImage = document.createElement("img");
slideshowImage.style.width = "100%";
slideshowImage.style.height = "100%";
slideshowImage.style.objectFit = "contain";
slideshowImage.style.border = "0";
slideshowImage.style.padding = "0";
slideshowImage.style.margin = "0";
getById("gridlayout").innerHTML = "";
getById("gridlayout").appendChild(slideshowImage);
}
var keys = Object.keys(session.rpcs);
if (!keys.length) {
slideshowActive = false;
return;
}
var video = false;
var key = false;
for (var i = 0; i < keys.length; i++) {
if (session.rpcs[keys[i]].videoElement) {
key = keys[i];
video = session.rpcs[keys[i]].videoElement;
break;
}
}
if (!video) {
slideshowCanvas = false;
slideshowActive = false;
return;
}
if (!slideshowCanvas) {
slideshowCanvas = document.createElement("canvas");
slideshowCanvas.getContext("2d").imageSmoothingEnabled = false;
session.requestResolution(key, session.viewwidth || 480, session.viewheight || 480);
}
slideshowCanvas.width = video.videoWidth;
slideshowCanvas.height = video.videoHeight;
slideshowCanvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoHeight); // for drawing the video element on the canvas
var image = slideshowCanvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); // here is the most important part because if you dont replace you will get a DOM 18 exception.
slideshowImage.src = image;
} catch (e) {
errorlog(e);
}
slideshowActive = false;
}
function setAvatarImage(tracks) {
if (session.avatar && session.avatar.ready) {
if (session.avatar && session.avatar.timer) {
clearInterval(session.avatar.timer);
session.avatar.timer = null;
}
setupCanvas();
var width = 512;
var height = 288;
var maxW = 1280;
var maxH = 720;
if (session.quality == 0) {
maxW = 1920;
maxH = 1080;
} else if (session.quality == 2) {
maxW = 640;
maxH = 360;
}
if (session.width) {
maxW = session.width;
}
if (session.height) {
maxH = session.height;
}
if (session.avatar.naturalHeight && session.avatar.naturalHeight > maxH) {
width = parseInt((maxH / session.avatar.naturalHeight) * session.avatar.naturalWidth);
height = maxH;
if (width > maxW) {
width = maxW;
height = parseInt((maxW / width) * height);
}
} else if (session.avatar.naturalWidth && session.avatar.naturalWidth > maxW) {
width = maxW;
height = parseInt((maxW / session.avatar.naturalWidth) * session.avatar.naturalHeight);
} else {
width = session.avatar.naturalWidth;
height = session.avatar.naturalHeight;
}
session.canvasSource.width = width;
session.canvasSource.height = height;
session.canvas.height = 2 * parseInt(height / 2);
session.canvas.width = 2 * parseInt(width / 2);
session.canvasCtx.drawImage(session.avatar, 0, 0, session.canvas.width, session.canvas.height);
session.avatar.timer = setInterval(function () {
log("drawing");
session.canvasCtx.drawImage(session.avatar, 0, 0, session.canvas.width, session.canvas.height);
}, 200); // too slow and it takes way too long for the video to udpate when a new guest joins
applyMirror(true);
session.avatar.tracks = session.canvas.captureStream().getVideoTracks();
return session.avatar.tracks;
}
applyMirror(session.mirrorExclude);
return tracks;
}
var drawOnScreenObject = null;
function drawOnScreen() {
var canvas = document.getElementById("drawOnScreen");
if (!canvas) {
canvas = document.createElement("canvas");
document.getElementById("gridlayout").appendChild(canvas);
document.getElementById("gridlayout").style.position = "relative";
} else {
return;
}
var ctx = canvas.getContext("2d");
canvas.width = parseInt(document.getElementById("gridlayout").clientWidth / 2);
canvas.height = parseInt(document.getElementById("gridlayout").clientHeight / 2);
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.style.display = "block";
canvas.style.position = "absolute";
canvas.style.bottom = "0";
canvas.style.left = "0";
var flag = false,
prevX = 0,
currX = 0,
prevY = 0,
currY = 0,
dot_flag = false;
var x = "black",
y = 2;
var object = {};
function findxy(res, e) {
if (res == "down") {
prevX = currX;
prevY = currY;
currX = e.clientX - canvas.offsetLeft;
currY = e.clientY - canvas.offsetTop;
flag = true;
dot_flag = true;
if (dot_flag) {
ctx.beginPath();
ctx.fillStyle = x;
ctx.fillRect(currX, currY, 2, 2);
ctx.closePath();
dot_flag = false;
}
}
if (res == "up" || res == "out") {
flag = false;
}
if (res == "move") {
if (flag) {
prevX = currX;
prevY = currY;
currX = e.clientX - canvas.offsetLeft;
currY = e.clientY - canvas.offsetTop;
draw();
}
}
}
function draw() {
ctx.beginPath();
var mx = canvas.width / parseInt(document.getElementById("gridlayout").clientWidth);
var my = canvas.height / parseInt(document.getElementById("gridlayout").clientHeight);
var mo = parseInt(document.getElementById("header").clientHeight);
ctx.moveTo(prevX * mx, prevY * my - mo * my);
ctx.lineTo(currX * mx, currY * my - mo * my);
ctx.strokeStyle = x;
ctx.lineWidth = y;
ctx.stroke();
ctx.closePath();
}
function onMouseMove(e) {
findxy("move", e);
}
function onMouseDown(e) {
findxy("down", e);
}
function onMouseUp(e) {
findxy("up", e);
}
function onMouseOut(e) {
findxy("out", e);
}
object.stop = function stop() {
canvas.removeEventListener("mousemove", onMouseMove, false);
canvas.removeEventListener("mousedown", onMouseDown, false);
canvas.removeEventListener("mouseup", onMouseUp, false);
canvas.removeEventListener("mouseout", onMouseOut, false);
canvas.remove();
document.getElementById("startDrawScreen").classList.remove("hidden");
document.querySelectorAll(".drawActive").forEach(ele => {
ele.classList.add("hidden");
});
drawOnScreenObject = null;
};
object.init = function init() {
canvas.addEventListener("mousemove", onMouseMove, false);
canvas.addEventListener("mousedown", onMouseDown, false);
canvas.addEventListener("mouseup", onMouseUp, false);
canvas.addEventListener("mouseout", onMouseOut, false);
document.getElementById("startDrawScreen").classList.add("hidden");
document.querySelectorAll(".drawActive").forEach(ele => {
ele.classList.remove("hidden");
});
};
object.color = function color(obj) {
switch (obj.dataset.color) {
case "green":
x = "green";
break;
case "blue":
x = "blue";
break;
case "red":
x = "red";
break;
case "yellow":
x = "yellow";
break;
case "orange":
x = "orange";
break;
case "black":
x = "black";
break;
case "white":
x = "white";
break;
}
if (x == "white") y = 14;
else y = 2;
};
object.erase = function erase() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
object.save = function save() {
var dataURL = canvas.toDataURL();
};
object.init();
drawOnScreenObject = object;
return object;
}
// SENDER DRAWERS LOGIC PORTION START
function fitCurve(points) {
if (points.length <= 1) return points;
if (points.length === 2) return [{t: 'l', p: [points[0], points[1]]}];
if (points.length === 3) return [{t: 'q', p: points}];
let result = [];
for (let i = 0; i < points.length - 1; i += 3) {
let p0 = points[i];
let p1 = points[i + 1] || p0;
let p2 = points[i + 2] || p1;
let p3 = points[i + 3] || p2;
result.push({
t: 'b',
p: [p0, p1, p2, p3]
});
}
return result;
}
function isSharpTurn(points) {
if (points.length < 3) return false;
let angle1 = Math.atan2(points[1].y - points[0].y, points[1].x - points[0].x);
let angle2 = Math.atan2(points[2].y - points[1].y, points[2].x - points[1].x);
let angleDiff = Math.abs(angle2 - angle1);
return angleDiff > Math.PI / 4;
}
function drawOnThis(video) {
try {
if (!video || !video.container) {
warnlog("no video holder; not compatible");
return;
}
var container = video.container || video.parentNode;
var holder = container.holder || null;
var canvas = document.createElement('canvas');
if (!holder) {
holder = document.createElement("div");
container.holder = holder;
holder.className = "holder";
holder.dataset.holder = true;
container.style = "display: flex;\
align-items: center;\
justify-content: center;";
container.appendChild(holder);
holder.appendChild(video);
video.style.setProperty('top', '0', 'important');
video.style.setProperty('left', '0', 'important');
session.windowed = false;
applyMirror();
canvas.style.position = "fixed";
holder.style = "position: relative;\
width: 800px;\
height: 450px; \
display: flex;\
align-items: center;\
justify-content: center;"
} else {
canvas.className = "drawingCanvas";
}
canvas.style.pointerEvents = "none";
holder.appendChild(canvas);
video.canvas = canvas;
const ctx = canvas.getContext('2d');
ctx.lineWidth = 3;
const buttonContainer = document.createElement('div');
const enableDrawingBtn = document.createElement('button');
const clearDrawingBtn = document.createElement('button');
const undoDrawingBtn = document.createElement('button'); // Undo button
enableDrawingBtn.textContent = "Enable Drawing";
clearDrawingBtn.textContent = "Clear";
undoDrawingBtn.textContent = "Undo"; // Undo button text
buttonContainer.className = "buttonContainer";
buttonContainer.appendChild(enableDrawingBtn);
buttonContainer.appendChild(clearDrawingBtn);
buttonContainer.appendChild(undoDrawingBtn); // Add undo button to container
holder.appendChild(buttonContainer);
let isDrawing = false;
let drawingEnabled = false;
let drawingData = [];
let lastPoint = null;
let lastSentTime = 0;
const sendInterval = 1000; // 1 second
let lastPoints = [];
function startDrawing(e) {
if (!drawingEnabled) return;
isDrawing = true;
draw(e);
}
function draw(e) {
if (!isDrawing || !drawingEnabled) return;
const rect = canvas.getBoundingClientRect();
let x = (e.clientX - rect.left) / rect.width;
let y = (e.clientY - rect.top) / rect.height;
// Check if the mouse is within bounds
if (x < 0 || x > 1 || y < 0 || y > 1) {
stopDrawing();
return;
}
ctx.lineCap = 'round';
ctx.strokeStyle = 'red';
const canvasX = x * canvas.width;
const canvasY = y * canvas.height;
if (lastPoint) {
ctx.beginPath();
ctx.moveTo(lastPoint.x * canvas.width, lastPoint.y * canvas.height);
ctx.lineTo(canvasX, canvasY);
ctx.stroke();
}
x = Math.round(x*4000)/4000;
y = Math.round(y*4000)/4000;
lastPoint = { x, y };
lastPoints.push({ x, y });
// Send data more frequently
if (lastPoints.length >= 5 || Date.now() - lastSentTime >= 50) {
sendDrawingData();
}
}
function redrawCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = "red";
let isNewPath = true;
for (let i = 0; i < drawingData.length; i++) {
let segment = drawingData[i];
if (!segment) {
// End of a path
ctx.stroke();
ctx.beginPath();
isNewPath = true;
continue;
}
if (segment.t) {
// This is a complex segment (line or bezier)
switch(segment.t) {
case 'b':
let [p0, p1, p2, p3] = segment.p;
if (isNewPath) {
ctx.moveTo(p0.x * canvas.width, p0.y * canvas.height);
isNewPath = false;
}
ctx.bezierCurveTo(
p1.x * canvas.width, p1.y * canvas.height,
p2.x * canvas.width, p2.y * canvas.height,
p3.x * canvas.width, p3.y * canvas.height
);
break;
case 'q':
let [q0, q1, q2] = segment.p;
if (isNewPath) {
ctx.moveTo(q0.x * canvas.width, q0.y * canvas.height);
isNewPath = false;
}
ctx.quadraticCurveTo(
q1.x * canvas.width, q1.y * canvas.height,
q2.x * canvas.width, q2.y * canvas.height
);
break;
case 'l':
let [l0, l1] = segment.p;
if (isNewPath) {
ctx.moveTo(l0.x * canvas.width, l0.y * canvas.height);
isNewPath = false;
}
ctx.lineTo(l1.x * canvas.width, l1.y * canvas.height);
break;
default:
warnlog(segment);
}
} else if (segment.x !== undefined && segment.y !== undefined) {
// This is a simple point
const canvasX = segment.x * canvas.width;
const canvasY = segment.y * canvas.height;
if (isNewPath) {
ctx.moveTo(canvasX, canvasY);
isNewPath = false;
} else {
ctx.lineTo(canvasX, canvasY);
}
}
}
ctx.stroke();
}
function processPoints(points) {
let processedPoints = [];
let currentSegment = [];
for (let i = 0; i < points.length; i++) {
if (points[i] === null) {
if (currentSegment.length > 0) {
if (currentSegment.length === 2) {
processedPoints.push({t: 'l', p: currentSegment});
} else {
processedPoints.push(...fitCurve(currentSegment));
}
currentSegment = [];
}
processedPoints.push(null);
} else if (i > 0 && isSignificantBreak(points[i-1], points[i])) {
if (currentSegment.length > 0) {
if (currentSegment.length === 2) {
processedPoints.push({t: 'l', p: currentSegment});
} else {
processedPoints.push(...fitCurve(currentSegment));
}
currentSegment = [];
}
currentSegment.push(points[i]);
} else {
currentSegment.push(points[i]);
if (currentSegment.length >= 4) {
processedPoints.push(...fitCurve(currentSegment));
currentSegment = [currentSegment[currentSegment.length - 1]];
}
}
}
if (currentSegment.length > 0) {
if (currentSegment.length === 2) {
processedPoints.push({t: 'l', p: currentSegment});
} else {
processedPoints.push(...fitCurve(currentSegment));
}
}
return processedPoints;
}
function isSignificantBreak(point1, point2) {
const distance = Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
return distance > 0.05; // Increased threshold
}
function processCurrentSegment(segment) {
if (segment.length < 3) return segment;
if (isSharpTurn(segment)) {
return segment;
} else {
return fitCurve(segment);
}
}
function drawBezierCurve(points) {
const [start, control1, control2, end] = points;
ctx.moveTo(start.x * canvas.width, start.y * canvas.height);
ctx.bezierCurveTo(
control1.x * canvas.width, control1.y * canvas.height,
control2.x * canvas.width, control2.y * canvas.height,
end.x * canvas.width, end.y * canvas.height
);
}
function sendDrawingData(alt = false) {
if (alt === "clear") {
drawingData = [];
if (video.id === "videosource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing) {
session.sendMessage({ draw: "clear"}, UUID);
}
}
} else if (video.id === "screensharesource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) {
session.sendMessage({ draw: "clear", altUUID:true}, UUID);
}
}
} else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
session.sendRequest({ draw: "clear" }, video.dataset.UUID);
}
return;
}
if (alt === "cleanup") {
drawingData = [];
if (video.id === "videosource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing) {
session.sendMessage({ draw: "cleanup"}, UUID);
}
}
} else if (video.id === "screensharesource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) {
session.sendMessage({ draw: "cleanup", altUUID:true}, UUID);
}
}
} else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
session.sendRequest({ draw: "cleanup" }, video.dataset.UUID);
}
return;
}
if (alt === "undo") {
if (drawingData.length > 0) {
// Find the last segment to remove
let found = false;
for (let i = drawingData.length - 1; i >= 0; i--) {
if (drawingData[i] === null) {
drawingData = drawingData.slice(0, i);
if (found){
drawingData.push(null);
break;
}
} else {
found = true;
}
if (i === 0) { // Handle case when there's no null in drawingData
drawingData = [];
}
}
redrawCanvas();
// Send the undo command
if (video.id === "videosource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing) {
session.sendMessage({ draw: "undo" }, UUID);
}
}
} else if (video.id === "screensharesource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) {
session.sendMessage({ draw: "undo", altUUID: true }, UUID);
}
}
} else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
session.sendRequest({ draw: "undo" }, video.dataset.UUID);
}
}
return;
}
if (alt === "sync") {
if (!drawingData.length){return;}
// Send the processed points
if (video.id === "videosource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing) {
if (!session.pcs[UUID].initialDrawing){
session.pcs[UUID].initialDrawing = true;
session.sendMessage({ draw: {p: drawingData}}, UUID);
}
}
}
} else if (video.id === "screensharesource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) {
if (!session.pcs[UUID].initialDrawing2){
session.pcs[UUID].initialDrawing2 = true;
session.sendMessage({ draw: {p: drawingData}, altUUID: true}, UUID);
}
}
}
} else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
if (!session.rpcs[video.dataset.UUID].initialDrawing){
session.rpcs[video.dataset.UUID].initialDrawing = true;
session.sendRequest({ draw: {p: drawingData} }, video.dataset.UUID);
}
}
return;
}
if (lastPoints.length > 0) {
var processedPoints = processPoints(lastPoints);
lastPoints = [];
lastSentTime = Date.now();
var dataToSend = {
p: processedPoints
};
drawingData.push(...processedPoints); // Store only points in drawingData
// Send the processed points with timestamp
if (video.id === "videosource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing) {
if (session.pcs[UUID].initialDrawing){
session.sendMessage({ draw: dataToSend}, UUID);
} else {
session.pcs[UUID].initialDrawing = true;
session.sendMessage({ draw: {p: drawingData}}, UUID);
}
}
}
} else if (video.id === "screensharesource") {
for (var UUID in session.pcs) {
if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) {
if (session.pcs[UUID].initialDrawing2){
session.sendMessage({ draw: dataToSend, altUUID: true}, UUID);
} else {
session.pcs[UUID].initialDrawing2 = true;
session.sendMessage({ draw: {p: drawingData}, altUUID: true}, UUID);
}
}
}
} else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing){
if (session.rpcs[video.dataset.UUID].initialDrawing){
session.sendRequest({ draw: dataToSend }, video.dataset.UUID);
} else {
session.rpcs[video.dataset.UUID].initialDrawing = true;
session.sendRequest({ draw: {p: drawingData} }, video.dataset.UUID);
}
}
}
}
function resizeCanvas() {
canvas.width = video.offsetWidth;
canvas.height = video.offsetHeight;
redrawCanvas();
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
video.addEventListener('resize', resizeCanvas);
enableDrawingBtn.addEventListener('click', () => {
drawingEnabled = !drawingEnabled;
enableDrawingBtn.textContent = drawingEnabled ? 'Disable Drawing' : 'Enable Drawing';
canvas.style.pointerEvents = drawingEnabled ? "auto" : "none";
});
clearDrawingBtn.addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawingData = [];
lastPoints = [];
sendDrawingData("clear");
});
undoDrawingBtn.addEventListener('click', () => {
sendDrawingData("undo");
});
function stopDrawing() {
if (isDrawing) {
isDrawing = false;
ctx.beginPath();
lastPoint = null;
lastPoints.push(null); // Add null to mark end of path
sendDrawingData();
}
}
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
canvas.addEventListener('mouseleave', stopDrawing);
canvas.addEventListener('mouseenter', (e) => {
if (e.buttons !== 1) { // If left mouse button is not pressed
stopDrawing();
}
});
function createCleanupFunction() {
return function cleanup() {
sendDrawingData("cleanup");
window.removeEventListener('resize', resizeCanvas);
video.removeEventListener('resize', resizeCanvas);
if (canvas) {
canvas.removeEventListener('mousedown', startDrawing);
canvas.removeEventListener('mousemove', draw);
canvas.removeEventListener('mouseup', stopDrawing);
canvas.removeEventListener('mouseout', stopDrawing);
canvas.removeEventListener('mouseleave', stopDrawing);
canvas.removeEventListener('mouseenter', stopDrawing);
if (canvas.parentNode) {
canvas.parentNode.removeChild(canvas);
}
}
if (buttonContainer && buttonContainer.parentNode) {
buttonContainer.parentNode.removeChild(buttonContainer);
}
};
}
function syncNewConnections() {
return function syncDrawing() {
setTimeout(()=>{
sendDrawingData("sync")
},4000);
}
}
video.syncDrawOnVideo = syncNewConnections();
video.clearDrawOnVideo = createCleanupFunction();
return video.clearDrawOnVideo;
} catch (e) {
errorlog(e);
}
}
// END SENDING LOGIC
// var cleanUp = drawOnThis(document.getElementById('videoElement'));
// cleanUp();
// START RECEIVING LOGIC
function receiveDrawingOnVideo(video, UUID = false) {
try {
if (!video || !video.container) {
warnlog("no video holder; not compatible");
return;
}
const canvas = document.createElement('canvas');
canvas.className = "drawingCanvas";
canvas.style.pointerEvents = "none";
var receivedDrawingData = [];
var container = video.parentNode;
if (!container) {
return;
}
container.appendChild(canvas);
var color = 'red';
if (UUID) {
color = getColorFromName(UUID);
}
function positionCanvas() {
const videoRect = video.getBoundingClientRect();
const computedStyle = getComputedStyle(video);
canvas.style.width = computedStyle.width;
canvas.style.height = computedStyle.height;
canvas.style.top = `${videoRect.top + window.scrollY}px`;
canvas.style.left = `${videoRect.left + window.scrollX}px`;
canvas.width = video.clientWidth;
canvas.height = video.clientHeight;
if (video.dataset.transform) {
canvas.style.transform = video.dataset.transform;
}
redrawCanvas();
}
const ctx = canvas.getContext('2d');
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.lineWidth = 3;
function resizeCanvas() {
canvas.width = video.offsetWidth;
canvas.height = video.offsetHeight;
if (video.dataset.transform) {
canvas.style.transform = video.dataset.transform;
}
redrawCanvas();
}
let observer = null;
if (!(video && video.container && video.container.holder)) {
positionCanvas();
window.addEventListener('resize', positionCanvas);
video.addEventListener('resize', positionCanvas);
observer = new ResizeObserver(positionCanvas);
observer.observe(video);
} else {
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
video.addEventListener('resize', resizeCanvas);
}
function drawBezierCurve(points) {
const [start, control1, control2, end] = points;
ctx.moveTo(start.x * canvas.width, start.y * canvas.height);
ctx.bezierCurveTo(
control1.x * canvas.width, control1.y * canvas.height,
control2.x * canvas.width, control2.y * canvas.height,
end.x * canvas.width, end.y * canvas.height
);
}
function redrawCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = color;
let isNewPath = true;
for (let i = 0; i < receivedDrawingData.length; i++) {
let segment = receivedDrawingData[i];
if (!segment) {
// End of a path
ctx.stroke();
ctx.beginPath();
isNewPath = true;
continue;
}
switch(segment.t) {
case 'b':
let [p0, p1, p2, p3] = segment.p;
if (isNewPath) {
ctx.moveTo(p0.x * canvas.width, p0.y * canvas.height);
isNewPath = false;
}
ctx.bezierCurveTo(
p1.x * canvas.width, p1.y * canvas.height,
p2.x * canvas.width, p2.y * canvas.height,
p3.x * canvas.width, p3.y * canvas.height
);
break;
case 'q':
let [q0, q1, q2] = segment.p;
if (isNewPath) {
ctx.moveTo(q0.x * canvas.width, q0.y * canvas.height);
isNewPath = false;
}
ctx.quadraticCurveTo(
q1.x * canvas.width, q1.y * canvas.height,
q2.x * canvas.width, q2.y * canvas.height
);
break;
case 'l':
let [l0, l1] = segment.p;
if (isNewPath) {
ctx.moveTo(l0.x * canvas.width, l0.y * canvas.height);
isNewPath = false;
}
ctx.lineTo(l1.x * canvas.width, l1.y * canvas.height);
break;
default:
// Fallback for non-curve points
if (isNewPath) {
ctx.moveTo(segment.x * canvas.width, segment.y * canvas.height);
isNewPath = false;
} else {
ctx.lineTo(segment.x * canvas.width, segment.y * canvas.height);
}
}
}
ctx.stroke();
}
function updateDrawing(newData) {
if (newData === "clear") {
receivedDrawingData = [];
ctx.clearRect(0, 0, canvas.width, canvas.height);
} else if (newData === "undo") {
if (receivedDrawingData.length > 0) {
// Find the last segment to remove
let found = false;
for (let i = receivedDrawingData.length - 1; i >= 0; i--) {
if (receivedDrawingData[i] === null) {
receivedDrawingData = receivedDrawingData.slice(0, i);
if (found){
receivedDrawingData.push(null);
break;
}
} else {
found = true;
}
if (i === 0) { // Handle case when there's no null in receivedDrawingData
receivedDrawingData = [];
}
}
redrawCanvas();
}
} else {
// Handle both new and old data formats
if (newData.p) {
// New format
receivedDrawingData.push(...newData.p);
} else if (Array.isArray(newData)) {
// Old format or array of points
receivedDrawingData.push(...newData);
} else if (typeof newData === 'object' && newData.x !== undefined && newData.y !== undefined) {
// Single point
receivedDrawingData.push(newData);
} else {
console.error("Unexpected data format:", newData);
return;
}
redrawCanvas();
}
}
function clearDrawing() {
receivedDrawingData = [];
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function cleanup() {
try {
if (observer) {
window.removeEventListener('resize', positionCanvas);
video.removeEventListener('resize', positionCanvas);
observer.disconnect();
} else {
window.removeEventListener('resize', resizeCanvas);
video.removeEventListener('resize', resizeCanvas);
}
clearDrawing();
} catch(e){
errorlog(e);
}
container.removeChild(canvas);
}
return {
updateDrawing,
clearDrawing,
cleanup
};
} catch (e) {
errorlog(e);
}
}
// END REMOTE DRAWING LOGIC
////////// Canvas Effects ///////////////
var drawFrameMirroredActive = false;
function drawFrameMirrored(mirror = true, flip = false) {
if (drawFrameMirroredActive) return;
drawFrameMirroredActive = true;
if (session.effect == "2") {
mirror = true;
flip = false;
} else if (session.effect == "-2") {
mirror = true;
flip = true;
} else if (session.effect == "-1") {
mirror = false;
flip = true;
}
try {
session.canvasCtx.save();
if (flip) {
session.canvasCtx.scale(mirror ? -1 : 1, -1);
session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * (mirror ? -1 : 1), session.canvas.height * -1);
} else {
session.canvasCtx.scale(mirror ? -1 : 1, 1);
session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * (mirror ? -1 : 1), session.canvas.height);
}
session.canvasCtx.restore();
} catch (e) {
errorlog(e);
}
drawFrameMirroredActive = false;
}
function motionDetection(video, threshold = 15, sensitivity = 75) {
var targetSize = 16;
if (!video.motionDetector) {
video.motionDetector = {};
video.motionDetector.canvas = document.createElement("canvas");
video.motionDetector.canvas.width = targetSize;
video.motionDetector.canvas.height = targetSize;
try {
video.motionDetector.ctx = video.motionDetector.canvas.getContext("2d", { willReadFrequently: true });
} catch (e) {
video.motionDetector.ctx = video.motionDetector.canvas.getContext("2d");
}
video.motionDetector.previous = [];
for (var y = 0; y < targetSize; y++) {
for (var x = 0; x < targetSize; x++) {
video.motionDetector.previous.push(0);
}
}
}
var motionDetector = video.motionDetector;
motionDetector.ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, targetSize, targetSize);
var data = motionDetector.ctx.getImageData(0, 0, targetSize, targetSize).data;
var matches = 0;
for (var y = 0; y < targetSize; y++) {
for (var x = 0; x < targetSize; x++) {
var pos = y * targetSize + x;
var pos2 = pos * 3;
var value = data[pos2] + data[pos2 + 1] + data[pos2 + 2]; // convert to to greyscale
if (motionDetector.previous[pos] && Math.abs(motionDetector.previous[pos] - value) > sensitivity) {
matches += 1;
}
motionDetector.previous[pos] = value;
}
}
if (matches >= threshold) {
log("MOTION DETECTED: " + matches);
if (session.motionSwitch && window.obsstudio && window.obsstudio["setCurrentScene"]) {
if (!changeSceneEnabled) {
// the bit cut scene change is already active.
if (session.obsState && session.obsState.details && session.obsState.details.thisScene && session.obsState.details.currentScene) {
if (session.obsState.details.thisScene !== session.obsState.details.currentScene.name) {
// don't trigger it multiple times; makes it hard to prep next scene
window.obsstudio["setCurrentScene"](session.obsState.details.thisScene);
}
}
}
}
pokeIframeAPI("motion-detected", true, video.dataset.UUID || true);
if (session.infocus !== (video.dataset.UUID || true)) {
if (!session.layout) {
session.infocus = video.dataset.UUID || true;
updateMixer();
}
}
if (session.motionRecord) {
if (!session.motionRecordTimeout) {
session.motionRecordTimeout = setTimeout(function () {
session.motionRecordTimeout = null;
}, 1000);
saveVideoFrameToDisk(video);
}
}
}
}
let currentOscillatorId = 0;
function clearOscillator() {
const thisOscillatorId = ++currentOscillatorId;
if (session.canvasOscillator) {
session.canvasOscillator.stop();
session.canvasOscillator.disconnect();
session.canvasOscillator = null;
}
if (session.canvasSilence) {
session.canvasSilence.disconnect();
session.canvasSilence = null;
}
if (session.stats) {
delete session.stats.canvas_draw_rate;
}
}
function setupOscillator(callbackFunction, frameRate = 30, timeOne = null, thisOscillatorId = null) {
if (!thisOscillatorId) {
thisOscillatorId = ++currentOscillatorId;
} else if (currentOscillatorId !== thisOscillatorId) {
return false;
}
if (session.canvasOscillator) {
session.canvasOscillator.stop();
session.canvasOscillator.disconnect();
session.canvasOscillator = null;
}
if (session.canvasSilence) {
session.canvasSilence.disconnect();
session.canvasSilence = null;
}
if (!session.audioCtx) {
session.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
let oscillator = session.audioCtx.createOscillator();
session.canvasOscillator = oscillator;
let silence = session.audioCtx.createGain();
session.canvasSilence = silence;
silence.gain.value = 0;
oscillator.connect(silence);
silence.connect(session.audioCtx.destination);
if (!timeOne) {
timeOne = session.audioCtx.currentTime;
}
oscillator.onended = () => {
oscillator.disconnect();
silence.disconnect();
if (currentOscillatorId === thisOscillatorId) {
let timeTwo = session.audioCtx.currentTime;
if (typeof callbackFunction === "function") {
callbackFunction();
}
if (session.stats) {
let actualRate = 1 / (timeTwo - timeOne); // Calculate the actual FPS
session.stats.canvas_draw_rate = parseInt(actualRate * 10) / 10;
}
setupOscillator(callbackFunction, frameRate, timeTwo, thisOscillatorId);
}
};
oscillator.start(timeOne);
oscillator.stop(timeOne + 1 / frameRate);
return function (check = false) {
if (check && currentOscillatorId !== thisOscillatorId) {
clearOscillator();
return true;
} else if (check) {
return false;
}
if (currentOscillatorId === thisOscillatorId) {
clearOscillator(); // clear only if needs to be cleared
}
return false;
};
}
function setupCanvas() {
clearOscillator();
log("SETUP CANVAS");
if (session.canvas === null) {
session.canvas = document.createElement("canvas");
session.canvas.width = 512;
session.canvas.height = 288;
try {
session.canvasCtx = session.canvas.getContext("2d", { alpha: true, willReadFrequently: true });
} catch (e) {
errorlog(e);
session.canvasCtx = session.canvas.getContext("2d");
}
session.canvasCtx.fillStyle = "black";
session.canvasCtx.fillRect(0, 0, 512, 288);
session.canvasSource = createVideoElement();
session.canvasSource.autoplay = true;
session.canvasSource.srcObject = createMediaStream();
session.canvasSource.id = "effectsVideoSource";
if (session.canvasSource.srcObject.getVideoTracks().length) {
session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
}
if (iOS || iPad) {
session.canvasSource.style.position = "absolute";
session.canvasSource.style.left = "0";
session.canvasSource.style.top = "0";
session.canvasSource.controls = session.showControls || false;
session.canvasSource.style.maxWidth = "1px";
session.canvasSource.style.maxHeight = "1px";
session.canvasSource.setAttribute("playsinline", "");
document.body.appendChild(session.canvasSource);
//session.canvasSource.play();
}
} else {
session.canvasSource.srcObject.getVideoTracks().forEach(function (trk) {
session.canvasSource.srcObject.removeTrack(trk);
});
}
}
function applyEffects(track) {
// video only please. do not touch audio. Run update Render Outpipe () instead of this directly.
log("applyEffects()");
if (session.effect == "0" || !session.effect) {
// auto align face
return track;
} else if (session.effect == "1") {
// auto align face
setupCanvas();
session.canvasSource.srcObject.addTrack(track);
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
drawFace();
} else if (session.effect == "7") {
// manual zoom
setupCanvas();
session.canvasSource.srcObject.addTrack(track);
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
digitalZoom(true);
} else if (["8", "overlay"].includes(session.effect)) {
// manual zoom
setupCanvas();
session.canvasSource.srcObject.addTrack(track);
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
simpleDraw();
} else if (["-2", "-1", "2"].includes(session.effect)) {
// mirror video at a canvas level
setupCanvas();
session.canvasSource.srcObject.addTrack(track);
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
setupOscillator(drawFrameMirrored, track.getSettings().frameRate || 30);
} else if (session.effect == "3" || session.effect == "4" || session.effect == "5") {
// blur & greenscreen (low and high)
setupCanvas();
session.canvasSource.srcObject.addTrack(track);
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
TFLiteWorker();
} else if (session.effect == "6") {
setupCanvas();
session.canvasSource.srcObject.addTrack(track);
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
if (session.canvasSource.readyState >= 3) {
mainMeshMask();
} else {
session.canvasSource.onloadeddata = mainMeshMask;
}
} else if (session.effect == "13") {
// ha, no way to turn it off once it's started, except to change cameras? not sure.
try {
track
.applyConstraints({ backgroundBlur: true })
.then(() => {
const settings = track.getSettings();
log(`Background blur is ${settings.backgroundBlur ? "ON" : "OFF"}`);
})
.catch(errorlog);
} catch (e) {
errorlog(e);
}
return track;
} else if (session.effect == "14" || session.effect == "15") {
// chroma key effects
setupCanvas();
session.canvasSource.srcObject.addTrack(track);
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
chromaKey();
} else {
if (session.canvasource) {
session.canvasSource.srcObject.getVideoTracks().forEach(function (trk) {
session.canvasSource.srcObject.removeTrack(trk);
});
} else {
session.canvasSource = createVideoElement();
session.canvasSource.srcObject = createMediaStream();
}
session.canvasSource.autoplay = true;
session.canvasSource.id = "effectsVideoSource";
session.canvasSource.srcObject.addTrack(track);
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
session.canvas.width = 512;
session.canvas.height = 288;
if (iOS || iPad) {
session.canvasSource.style.position = "absolute";
session.canvasSource.style.left = "0";
session.canvasSource.style.top = "0";
session.canvasSource.style.maxWidth = "1px";
session.canvasSource.style.maxHeight = "1px";
session.canvasSource.controls = session.showControls || false;
session.canvasSource.setAttribute("playsinline", "");
document.body.appendChild(session.canvasSource);
//session.canvasSource.play();
}
try {
JEELIZFACEFILTER.destroy();
} catch (e) {}
if (session.canvasWebGL) {
session.canvasWebGL.remove();
session.canvasWebGL = null;
}
session.canvasWebGL = document.createElement("canvas");
session.canvasWebGL.width = track.getSettings().width || 1280;
session.canvasWebGL.height = track.getSettings().height || 720;
session.canvasWebGL.id = "effectsCanvasTarget";
session.canvasWebGL.style.position = "fixed";
session.canvasWebGL.style.top = "-9999px";
session.canvasWebGL.style.left = "-9999px";
document.body.appendChild(session.canvasWebGL);
loadEffect(session.effect);
return session.canvasWebGL.captureStream().getVideoTracks()[0];
}
try {
return session.canvas.captureStream().getVideoTracks()[0];
} catch (e) {
if (!session.cleanOutput) {
warnUser(getTranslation("not-clean-session"), false, false);
}
return track;
}
}
function dataURItoArraybuffer(dataURI) {
var byteString = atob(dataURI.split(",")[1]);
var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return ab;
}
var makeImagesActive = null;
async function makeImages(startup = false) {
if (!session.webp) {
return;
} else if (makeImagesActive === true) {
return;
} else if (!session.videoElement) {
return;
} else if (session.videoMuted) {
return;
}
var stream = session.getLocalStream();
if (!stream || !stream.getVideoTracks().length) {
errorlog("No video element; can't make images for webp mode");
if (makeImagesActive) {
var exit = true;
for (var i in session.pcs) {
if (session.pcs[i].allowWebp) {
// just for safety, to avoid a race condition, double check that it's still not active.
exit = false;
}
}
if (exit) {
makeImagesActive = false;
return;
}
if (session.webPcanvas.makeImagesTimeout) {
session.webPcanvas.makeImagesTimeout.onended = null;
session.webPcanvas.makeImagesTimeout = null;
}
var osc = session.webPcanvas.aCtx.createOscillator();
osc.connect(session.webPcanvas.silence);
session.webPcanvas.makeImagesTimeout = osc;
osc.start(0);
osc.onended = function () {
this.disconnect();
makeImages();
};
osc.stop(session.webPcanvas.aCtx.currentTime + 0.5);
}
return;
}
if (makeImagesActive === null) {
makeImagesActive = true;
session.webPcanvas = document.createElement("canvas");
session.webPcanvas.makeImagesTimeout = null;
session.webPcanvas.aCtx = new AudioContext();
session.webPcanvas.silence = session.webPcanvas.aCtx.createGain();
session.webPcanvas.silence.gain.value = 0;
session.webPcanvas.silence.connect(session.webPcanvas.aCtx.destination);
session.webPcanvas.nowTime = new Date().getTime();
session.webPcanvasCtx = session.webPcanvas.getContext("2d", { alpha: session.alpha });
} else {
if (session.webPcanvas.makeImagesTimeout) {
session.webPcanvas.makeImagesTimeout.onended = null;
}
makeImagesActive = true;
}
if (startup) {
var exit = true;
for (var i in session.pcs) {
if (session.pcs[i].allowWebp) {
// just for safety, to avoid a race condition, double check that it's still not active.
exit = false;
}
}
if (exit) {
makeImagesActive = false;
return;
}
log("MAKE IMAGES STARTING?");
}
var track = stream.getVideoTracks()[0];
var settings = track.getSettings();
try {
var broadcasting = false;
var arrayBuffer = false;
var width = 480;
var height = 270;
var timeout = settings.frameRate > 24 ? 1000 / 24 : 1000 / settings.frameRate; // the answer to everything.
var quality = 0.66;
if (session.webPquality === 0) {
width = 1920;
height = 1080;
timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
} else if (session.webPquality === 1) {
width = 1280;
height = 720;
timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
} else if (session.webPquality === 2) {
width = 960;
height = 540;
timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
} else if (session.webPquality === 3) {
width = 853;
height = 480;
timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
} else if (session.webPquality === 4) {
width = 640;
height = 360;
timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
} else if (session.webPquality === 5) {
width = 480;
height = 270;
timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate;
} else if (session.webPquality === 6) {
width = 480;
height = 270;
timeout = 1000 / 15;
} else if (session.webPquality === 7) {
width = 480;
height = 270;
timeout = 1000 / 5;
} else if (session.webPquality === 8) {
width = 480;
height = 270;
timeout = 1000 / 3;
} else if (session.webPquality === 9) {
width = 640;
height = 360;
timeout = 1000;
}
session.webPcanvas.timeout = timeout;
session.webPcanvas.quality = quality;
if (settings.width < width) {
session.webPcanvas.width = settings.width;
session.webPcanvasCtx.width = settings.width;
} else {
session.webPcanvas.width = width;
session.webPcanvasCtx.width = width;
}
if (settings.height < height) {
session.webPcanvas.height = settings.height;
session.webPcanvasCtx.height = settings.height;
} else {
session.webPcanvas.height = height;
session.webPcanvasCtx.height = height;
}
var ar = session.webPcanvas.width / session.webPcanvas.height;
if (session.forceAspectRatio && session.forceAspectRatio > ar) {
session.webPcanvas.width = session.webPcanvas.height * session.forceAspectRatio;
} else if (session.forceAspectRatio && session.forceAspectRatio <= ar) {
session.webPcanvas.height = session.webPcanvas.width * session.forceAspectRatio;
}
for (var i in session.pcs) {
try {
if (session.pcs[i].allowWebp) {
// only publish to those seeking this stream
broadcasting = true;
if (!session.pcs[i].sendChannel.bufferedAmount) {
try {
if (!arrayBuffer) {
session.webPcanvasCtx.drawImage(session.videoElement, 0, 0, session.webPcanvas.width, session.webPcanvas.height);
arrayBuffer = dataURItoArraybuffer(session.webPcanvas.toDataURL("image/" + session.webp, session.webPcanvas.quality));
}
session.pcs[i].sendChannel.send(arrayBuffer);
} catch (e) {
errorlog(e);
}
}
}
} catch (e) {}
}
} catch (e) {
errorlog(e);
makeImagesActive = false;
return;
}
makeImagesActive = false;
session.webPcanvas.lastTime = session.webPcanvas.nowTime;
session.webPcanvas.nowTime = new Date().getTime();
if (broadcasting) {
// wait a bit of time, now that we sent a frame out.
var time = session.webPcanvas.timeout - (session.webPcanvas.nowTime - session.webPcanvas.lastTime);
if (time <= 0) {
var osc = session.webPcanvas.aCtx.createOscillator();
osc.connect(session.webPcanvas.silence);
session.webPcanvas.makeImagesTimeout = osc;
osc.start(0);
osc.onended = function () {
this.disconnect();
makeImages();
};
osc.stop(session.webPcanvas.aCtx.currentTime);
//session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},0);
} else {
var osc = session.webPcanvas.aCtx.createOscillator();
osc.connect(session.webPcanvas.silence);
session.webPcanvas.makeImagesTimeout = osc;
osc.start(0);
osc.onended = function () {
this.disconnect();
makeImages();
};
osc.stop(session.webPcanvas.aCtx.currentTime + time / 1000);
//session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},time);
}
} else {
// just double check that we shoulnd't be broadcasting.
for (var i in session.pcs) {
if (session.pcs[i].allowWebp) {
var osc = session.webPcanvas.aCtx.createOscillator();
osc.connect(session.webPcanvas.silence);
session.webPcanvas.makeImagesTimeout = osc;
osc.start(0);
osc.onended = function () {
this.disconnect();
makeImages();
};
osc.stop(session.webPcanvas.aCtx.currentTime + time / 1000);
//session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},0);
return;
}
}
log("Stopping webP broadcast.");
}
}
var updateUserListTimeout = null;
var updateUserListActive = false;
function updateUserList() {
if (session.showList === true) {
// continue
} else if (session.showList !== true && (session.cleanOutput || session.scene !== false || !session.roomid || session.director || session.showList === false)) {
return;
}
clearInterval(updateUserListTimeout);
updateUserListTimeout = setTimeout(function () {
if (updateUserListActive) {
return;
}
updateUserListActive = true;
try {
var added = false;
getById("userList").innerHTML = "";
for (var UUID in session.rpcs) {
if ((session.rpcs[UUID].videoElement && session.rpcs[UUID].streamSrc && session.rpcs[UUID].streamSrc.getTracks().length) || session.rpcs[UUID].canvas || session.rpcs[UUID].imageElement) {
if (session.rpcs[UUID].videoElement && document.body.contains(session.rpcs[UUID].videoElement)) {
continue;
} else if (session.rpcs[UUID].canvas && document.body.contains(session.rpcs[UUID].canvas)) {
continue;
} else if (session.rpcs[UUID].imageElement && document.body.contains(session.rpcs[UUID].imageElement)) {
continue;
}
}
if (session.rpcs[UUID].virtualHangup) {
// end of screen share / director ?
continue;
}
if (session.rpcs[UUID].videoMuted || (!session.rpcs[UUID].imageElement && !session.rpcs[UUID].canvas) || (session.infocus && session.infocus !== UUID) || (!session.rpcs[UUID].defaultSpeaker && session.activeSpeaker)) {
if (session.directorList.indexOf(UUID) >= 0) {
if (!session.rpcs[UUID].streamSrc) {
// director not active yet, so we won't bother showing it.
continue;
}
}
var insert = document.createElement("div");
if (session.rpcs[UUID].label) {
insert.innerText = session.rpcs[UUID].label.split("\\n")[0] + "";
} else if (session.directorList.indexOf(UUID) >= 0) {
miniTranslate(insert, "director");
//insert.innerHTML = getTranslation("director");
} else {
miniTranslate(insert, "unknown-user");
//insert.innerHTML = getTranslation("unknown-user");
}
try {
insert.dataset.UUID = UUID;
insert.dataset.sid = session.rpcs[UUID].streamID;
insert.title = "Stream ID: " + session.rpcs[UUID].streamID;
insert.addEventListener("click", function (e) {
// show stats of video if double clicked
log("clicked");
try {
if (session.statsMenu !== false) {
var uid = e.currentTarget.dataset.UUID;
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
e.stopPropagation();
return false;
}
}
} catch (e) {
errorlog(e);
}
});
if (session.statsMenu) {
if ("stats" in session.rpcs[UUID]) {
if (getById("menuStatsBox")) {
clearInterval(getById("menuStatsBox").interval);
getById("menuStatsBox").remove();
}
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, UUID);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID);
}
}
} catch (e) {}
getById("userList").appendChild(insert);
if (session.rpcs[UUID].videoElement) {
var volumeBarInsert = document.createElement("input");
volumeBarInsert.type = "range";
volumeBarInsert.className = "hidden";
volumeBarInsert.setAttribute("orient", "vertical");
volumeBarInsert.max = 100;
volumeBarInsert.min = 0;
volumeBarInsert.step = 1;
volumeBarInsert.dataset.UUID = UUID;
volumeBarInsert.setAttribute("value", parseFloat(session.rpcs[UUID].videoElement.volume) * 100);
volumeBarInsert.title = volumeBarInsert.value + "%";
volumeBarInsert.oninput = function (e) {
session.rpcs[this.dataset.UUID].videoElement.volume = parseInt(this.value) / 100 || 0;
if (!session.rpcs[this.dataset.UUID].videoElement.volume) {
this.parentNode.classList.add("red");
} else {
this.parentNode.classList.remove("red");
}
};
volumeBarInsert.onchange = function (e) {
this.parentNode.querySelector("input").classList.toggle("hidden");
};
var volumeButton = document.createElement("i");
volumeButton.className = "las la-volume-up";
volumeButton.onclick = function () {
this.parentNode.querySelector("input").classList.toggle("hidden");
this.parentNode.volumeBarInsert.focus();
};
var volumeInsert = document.createElement("div");
volumeInsert.className = "volume-control-userlist";
volumeInsert.volumeBarInsert = volumeBarInsert;
insert.appendChild(volumeInsert);
volumeInsert.appendChild(volumeBarInsert);
volumeInsert.appendChild(volumeButton);
}
if (session.rpcs[UUID].remoteMuteState || !session.rpcs[UUID].streamSrc) {
var muteInsert = document.createElement("div");
muteInsert.className = "video-mute-state-userlist";
muteInsert.innerHTML = '';
insert.appendChild(muteInsert);
} else if (session.rpcs[UUID].voiceMeter) {
insert.appendChild(session.rpcs[UUID].voiceMeter);
}
//getById("userList").innerHTML += " ";
added = true;
}
}
if (!added) {
getById("connectUsers").style.display = "none";
} else {
getById("connectUsers").style.display = "block";
}
} catch (e) {}
updateUserListActive = false;
}, 200);
}
function resetCanvas() {
log("resetCanvas();");
if (!session.streamSrc) {
checkBasicStreamsExist();
return;
}
session.streamSrc.getVideoTracks().forEach(track => {
session.canvasSource.width = track.getSettings().width || 1280;
session.canvasSource.height = track.getSettings().height || 720;
});
}
function initEffectsImage(){
if (!session.effectsImage) {
if (!session.selectedImage_contents) {
session.selectedImage_contents = getById("selectImage_contents");
}
if (session.selectedImage_contents.querySelector("img")) {
session.effectsImage = session.selectedImage_contents.querySelector("img");
session.effectsImage.classList.add("selectedContentEffectsImage");
} else if (session.defaultBackgroundImages && session.defaultBackgroundImages.length) {
session.effectsImage = document.createElement("img");
session.effectsImage.onload = function () {
URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory
};
session.effectsImage.src = session.defaultBackgroundImages[0];
session.effectsImage.classList.add("selectedContentEffectsImage");
} else {
session.effectsImage = document.createElement("img");
session.effectsImage.onload = function () {
URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory
};
session.effectsImage.src = "./media/bg_sample.webp";
}
}
}
var LaunchTFWorkerCallback = false;
function TFLiteWorker() {
if (session.tfliteModule == false) {
LaunchTFWorkerCallback = true;
return;
}
if (TFLITELOADING) {
LaunchTFWorkerCallback = true;
return;
}
LaunchTFWorkerCallback = false;
log("TFLiteWorker() called");
initEffectsImage();
//if (session.tfliteModule.looping){return;}
const segmentationWidth = 256;
const segmentationHeight = 144;
const segmentationPixelCount = segmentationWidth * segmentationHeight;
const inputMemoryOffset = session.tfliteModule._getInputMemoryOffset() / 4;
const outputMemoryOffset = session.tfliteModule._getOutputMemoryOffset() / 4;
const segmentationMask = new ImageData(segmentationWidth, segmentationHeight);
const segmentationMaskCanvas = document.createElement("canvas");
segmentationMaskCanvas.width = segmentationWidth;
segmentationMaskCanvas.height = segmentationHeight;
const segmentationMaskCtx = segmentationMaskCanvas.getContext("2d", { alpha: true, willReadFrequently: true });
session.tfliteModule.nowTime = new Date().getTime();
session.tfliteModule.offsetTime = 0;
var slow = 0;
var slower = false;
async function process() {
if (!(session.effect == "3" || session.effect == "4" || session.effect == "5")) {
//session.tfliteModule.looping=false;
errorlog("shouldn't happen");
return;
}
if (session.tfliteModule.activelyProcessing) {
return;
}
session.tfliteModule.activelyProcessing = true;
if (session.mobile) {
if (screenWidth !== window.innerWidth) {
screenWidth = window.innerWidth;
//session.tfliteModule.looping=false;
session.tfliteModule.activelyProcessing = false;
setTimeout(function () {
updateRenderOutpipe();
}, 200);
return;
}
}
try {
segmentationMaskCtx.filter = "none";
segmentationMaskCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, segmentationWidth, segmentationHeight);
const imageData = segmentationMaskCtx.getImageData(0, 0, segmentationWidth, segmentationHeight);
for (let i = 0; i < segmentationPixelCount; i++) {
session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3] = imageData.data[i * 4] / 255;
session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 1] = imageData.data[i * 4 + 1] / 255;
session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 2] = imageData.data[i * 4 + 2] / 255;
}
session.tfliteModule._runInference();
for (let i = 0; i < segmentationPixelCount; i++) {
const background = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2];
const person = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2 + 1];
const shift = Math.max(background, person);
const backgroundExp = Math.exp(background - shift);
const personExp = Math.exp(person - shift);
segmentationMask.data[i * 4 + 3] = Math.min(Math.pow((255 * personExp) / (backgroundExp + personExp), 1.5) - 10, 255); // softmax
}
segmentationMaskCtx.putImageData(segmentationMask, 0, 0);
session.canvasCtx.globalCompositeOperation = "copy";
if ((session.mobile && !session.flagship) || slower) {
session.canvasCtx.filter = "blur(4px)";
} else {
session.canvasCtx.filter = "blur(8px)";
}
session.canvasCtx.drawImage(segmentationMaskCanvas, 0, 0, segmentationWidth, segmentationHeight, 0, 0, session.canvasSource.width, session.canvasSource.height);
session.canvasCtx.globalCompositeOperation = "source-in";
session.canvasCtx.filter = "none";
session.canvasCtx.drawImage(session.canvasSource, 0, 0);
session.canvasCtx.globalCompositeOperation = "destination-over";
if (session.effect == "4") {
// greenscreen
session.canvasCtx.filter = "none";
session.canvasCtx.fillStyle = "#0F0";
session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height);
} else if (session.effect == "5") {
session.canvasCtx.filter = "none";
if (session.effectsImage.complete) {
try {
session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height);
} catch (e) {}
}
} else if (session.effect == "3") {
// BLUR
if (session.effectValue) {
session.canvasCtx.filter = "blur(" + parseInt(session.effectValue) * 2 + "px)";
} else {
session.canvasCtx.filter = "blur(4px)"; // Does not work on Safari
}
session.canvasCtx.drawImage(session.canvasSource, 0, 0);
session.canvasCtx.filter = "none";
} else {
session.tfliteModule.activelyProcessing = false;
//session.tfliteModule.looping=false;
return;
}
//////
} catch (e) {
errorlog(e);
session.tfliteModule.activelyProcessing = false;
//session.tfliteModule.looping=false;
return;
}
session.tfliteModule.lastTime = session.tfliteModule.nowTime;
session.tfliteModule.nowTime = new Date().getTime();
var time = 30 - (session.tfliteModule.nowTime - session.tfliteModule.lastTime || 0);
time = time + (session.tfliteModule.offsetTime || 0);
session.tfliteModule.activelyProcessing = false;
slow -= 1;
if (time <= 0) {
if (time < -40) {
slow += 1;
if (slow > 100) {
slower = true;
}
}
session.tfliteModule.offsetTime = 0;
} else {
slow -= 2;
session.tfliteModule.offsetTime = time || 0;
}
}
async function processiOS() {
if (!(session.effect == "3" || session.effect == "4" || session.effect == "5")) {
errorlog("shouldn't happen");
//session.tfliteModule.looping=false;
return;
}
if (session.tfliteModule.activelyProcessing) {
return;
}
session.tfliteModule.activelyProcessing = true;
if (screenWidth !== window.innerWidth) {
screenWidth = window.innerWidth;
setTimeout(function () {
updateRenderOutpipe();
}, 200);
//session.tfliteModule.looping=false;
session.tfliteModule.activelyProcessing = false;
return;
}
try {
segmentationMaskCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, segmentationWidth, segmentationHeight);
var imageData = segmentationMaskCtx.getImageData(0, 0, segmentationWidth, segmentationHeight);
for (let i = 0; i < segmentationPixelCount; i++) {
session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3] = imageData.data[i * 4] / 255;
session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 1] = imageData.data[i * 4 + 1] / 255;
session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 2] = imageData.data[i * 4 + 2] / 255;
}
session.tfliteModule._runInference();
for (let i = 0; i < segmentationPixelCount; i++) {
const background = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2];
const person = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2 + 1];
const shift = Math.max(background, person);
const backgroundExp = Math.exp(background - shift);
const personExp = Math.exp(person - shift);
segmentationMask.data[i * 4 + 3] = 255 - (255 * personExp) / (backgroundExp + personExp); // softmax
}
segmentationMaskCtx.putImageData(segmentationMask, 0, 0);
session.canvasCtx.globalCompositeOperation = "copy";
session.canvasCtx.drawImage(session.canvasSource, 0, 0);
session.canvasCtx.globalCompositeOperation = "destination-out";
session.canvasCtx.drawImage(segmentationMaskCanvas, 0, 0, segmentationWidth, segmentationHeight, 0, 0, session.canvasSource.width, session.canvasSource.height);
session.canvasCtx.globalCompositeOperation = "destination-over";
if (session.effect == "4") {
// greenscreen
session.canvasCtx.fillStyle = "#0F0";
session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height);
} else if (session.effect == "5") {
if (session.effectsImage.complete) {
try {
session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height);
} catch (e) {}
}
} else if (session.effect == "3") {
// BLUR
const width = canvasBG.width;
const height = canvasBG.height;
ctxBG.drawImage(session.canvasSource, 0, 0, width, height);
imageData = ctxBG.getImageData(0, 0, width, height);
const { data } = imageData;
// THE BELOW BLUR CODE polyfil is by David Enke
// MIT License: Copyright (c) 2019
// https://github.com/steveseguin/context-filter-polyfill/blob/master/src/filters/blur.filter.ts
const wm = width - 1;
const hm = height - 1;
const rad1 = amount + 1;
const r = [];
const g = [];
const b = [];
//const a = [];
const vmin = [];
const vmax = [];
let iterations = 3; // 1 - 3
let p, p1, p2;
while (iterations-- > 0) {
let yw = 0;
let yi = 0;
for (let y = 0; y < height; y++) {
let rsum = data[yw] * rad1;
let gsum = data[yw + 1] * rad1;
let bsum = data[yw + 2] * rad1;
for (let i = 1; i <= amount; i++) {
p = yw + ((i > wm ? wm : i) << 2);
rsum += data[p++];
gsum += data[p++];
bsum += data[p++];
}
for (let x = 0; x < width; x++) {
r[yi] = rsum;
g[yi] = gsum;
b[yi] = bsum;
if (y === 0) {
vmin[x] = ((p = x + rad1) < wm ? p : wm) << 2;
vmax[x] = (p = x - amount) > 0 ? p << 2 : 0;
}
p1 = yw + vmin[x];
p2 = yw + vmax[x];
rsum += data[p1++] - data[p2++];
gsum += data[p1++] - data[p2++];
bsum += data[p1++] - data[p2++];
yi++;
}
yw += width << 2;
}
for (let x = 0; x < width; x++) {
let yp = x;
let rsum = r[yp] * rad1;
let gsum = g[yp] * rad1;
let bsum = b[yp] * rad1;
for (let i = 1; i <= amount; i++) {
yp += i > hm ? 0 : width;
rsum += r[yp];
gsum += g[yp];
bsum += b[yp];
}
yi = x << 2;
for (let y = 0; y < height; y++) {
data[yi] = (rsum * mulSum) >>> shgSum;
data[yi + 1] = (gsum * mulSum) >>> shgSum;
data[yi + 2] = (bsum * mulSum) >>> shgSum;
if (x === 0) {
vmin[y] = ((p = y + rad1) < hm ? p : hm) * width;
vmax[y] = (p = y - amount) > 0 ? p * width : 0;
}
p1 = x + vmin[y];
p2 = x + vmax[y];
rsum += r[p1] - r[p2];
gsum += g[p1] - g[p2];
bsum += b[p1] - b[p2];
yi += width << 2;
}
}
}
////////////// END OF BLUR CODE - MIT LICENCED.
ctxBG.putImageData(imageData, 0, 0);
session.canvasCtx.drawImage(canvasBG, 0, 0, width, height, 0, 0, session.canvas.width, session.canvas.height);
} else {
session.tfliteModule.activelyProcessing = false;
//session.tfliteModule.looping=false;
return;
}
} catch (e) {
session.tfliteModule.activelyProcessing = false;
//session.tfliteModule.looping=false;
errorlog(e);
return;
}
session.tfliteModule.lastTime = session.tfliteModule.nowTime;
session.tfliteModule.nowTime = new Date().getTime();
var time = 30 - (session.tfliteModule.nowTime - session.tfliteModule.lastTime || 0);
time = time + (session.tfliteModule.offsetTime || 0);
slow -= 1;
if (time <= 0) {
if (time < -40) {
slow += 1;
if (slow > 100) {
slower = true;
}
}
session.tfliteModule.offsetTime = 0;
} else {
slow -= 2;
session.tfliteModule.offsetTime = time || 0;
}
session.tfliteModule.activelyProcessing = false;
}
//session.tfliteModule.looping=true;
var screenWidth = window.innerWidth;
if (iOS || iPad || SafariVersion) {
var canvasBG = document.createElement("canvas");
var ctxBG = canvasBG.getContext("2d", { alpha: false });
var amount = 1.0;
var mulTable = [1, 57, 41, 21, 203, 34, 97, 73, 227, 91, 149, 62, 105, 45, 39, 137, 241, 107, 3, 173, 39, 71, 65, 238, 219, 101, 187, 87, 81, 151, 141, 133, 249, 117, 221, 209, 197, 187, 177, 169, 5, 153, 73, 139, 133, 127, 243, 233, 223, 107, 103, 99, 191, 23, 177, 171, 165, 159, 77, 149, 9, 139, 135, 131, 253, 245, 119, 231, 224, 109, 211, 103, 25, 195, 189, 23, 45, 175, 171, 83, 81, 79, 155, 151, 147, 9, 141, 137, 67, 131, 129, 251, 123, 30, 235, 115, 113, 221, 217, 53, 13, 51, 50, 49, 193, 189, 185, 91, 179, 175, 43, 169, 83, 163, 5, 79, 155, 19, 75, 147, 145, 143, 35, 69, 17, 67, 33, 65, 255, 251, 247, 243, 239, 59, 29, 229, 113, 111, 219, 27, 213, 105, 207, 51, 201, 199, 49, 193, 191, 47, 93, 183, 181, 179, 11, 87, 43, 85, 167, 165, 163, 161, 159, 157, 155, 77, 19, 75, 37, 73, 145, 143, 141, 35, 138, 137, 135, 67, 33, 131, 129, 255, 63, 250, 247, 61, 121, 239, 237, 117, 29, 229, 227, 225, 111, 55, 109, 216, 213, 211, 209, 207, 205, 203, 201, 199, 197, 195, 193, 48, 190, 47, 93, 185, 183, 181, 179, 178, 176, 175, 173, 171, 85, 21, 167, 165, 41, 163, 161, 5, 79, 157, 78, 154, 153, 19, 75, 149, 74, 147, 73, 144, 143, 71, 141, 140, 139, 137, 17, 135, 134, 133, 66, 131, 65, 129, 1];
var mulSum = mulTable[amount];
var shgTable = [0, 9, 10, 10, 14, 12, 14, 14, 16, 15, 16, 15, 16, 15, 15, 17, 18, 17, 12, 18, 16, 17, 17, 19, 19, 18, 19, 18, 18, 19, 19, 19, 20, 19, 20, 20, 20, 20, 20, 20, 15, 20, 19, 20, 20, 20, 21, 21, 21, 20, 20, 20, 21, 18, 21, 21, 21, 21, 20, 21, 17, 21, 21, 21, 22, 22, 21, 22, 22, 21, 22, 21, 19, 22, 22, 19, 20, 22, 22, 21, 21, 21, 22, 22, 22, 18, 22, 22, 21, 22, 22, 23, 22, 20, 23, 22, 22, 23, 23, 21, 19, 21, 21, 21, 23, 23, 23, 22, 23, 23, 21, 23, 22, 23, 18, 22, 23, 20, 22, 23, 23, 23, 21, 22, 20, 22, 21, 22, 24, 24, 24, 24, 24, 22, 21, 24, 23, 23, 24, 21, 24, 23, 24, 22, 24, 24, 22, 24, 24, 22, 23, 24, 24, 24, 20, 23, 22, 23, 24, 24, 24, 24, 24, 24, 24, 23, 21, 23, 22, 23, 24, 24, 24, 22, 24, 24, 24, 23, 22, 24, 24, 25, 23, 25, 25, 23, 24, 25, 25, 24, 22, 25, 25, 25, 24, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 25, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 22, 25, 25, 23, 25, 25, 20, 24, 25, 24, 25, 25, 22, 24, 25, 24, 25, 24, 25, 25, 24, 25, 25, 25, 25, 22, 25, 25, 25, 24, 25, 24, 25, 18];
var shgSum = shgTable[amount];
log("session.canvas: " + session.canvas.width + "x" + session.canvas.height);
canvasBG.width = parseInt(session.canvas.width / 12);
canvasBG.height = parseInt(session.canvas.height / 12);
ctxBG.width = canvasBG.width;
ctxBG.height = canvasBG.height;
try {
session.tfliteModule.stopOscillator = setupOscillator(processiOS, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
errorlog(e);
session.tfliteModule.stopOscillator = setupOscillator(processiOS, 30);
}
} else {
try {
session.tfliteModule.stopOscillator = setupOscillator(process, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
errorlog(e);
session.tfliteModule.stopOscillator = setupOscillator(process, 30);
}
}
}
var insertableStreamWorker = null;
function setupSenderTransform(sender, UUID = false) {
if (!insertableStreamWorker) {
insertableStreamWorker = new Worker("./insertableStreamWorker.js", { name: "Insertable Stream worker" });
insertableStreamWorker.onmessage = event => {
if (event.data === "insertableStreamWorkerLoaded") {
if (session.encodedInsertableStreams == "e2ee") {
if (session.password) {
insertableStreamWorker.postMessage({ cryptoPhrase: session.password + session.salt + "aDdedSaLt123" }); // salt ⚔️ rainbow
} else {
insertableStreamWorker.postMessage({ cryptoKey: "aabbccddeeff00112233445566778899" });
}
} else if (session.encodedInsertableStreams == "lyra") {
insertableStreamWorker.postMessage({
lyraCodecModule: session.lyraCodecModule
});
}
} else {
console.log(event.data);
}
};
}
try {
const senderStreams = sender.createEncodedStreams();
const { readable, writable } = senderStreams;
let operation = "pass";
if (session.encodedInsertableStreams == "e2ee") {
operation = "encode";
} else if (session.encodedInsertableStreams == "red") {
operation = "redencode";
} else if (session.encodedInsertableStreams == "lyra" && sender.track && sender.track.kind === "audio") {
operation = "lyraencode";
} else if (UUID && session.pcs[UUID] && session.pcs[UUID].preferAudioCodec == "lyra" && sender.track && sender.track.kind === "audio") {
operation = "lyraencode";
}
insertableStreamWorker.postMessage(
{
operation: operation,
readable,
writable
},
[readable, writable]
);
} catch (e) {
errorlog(e);
}
}
function setupReceiverTransform(receiver, UUID = false) {
if (!insertableStreamWorker) {
insertableStreamWorker = new Worker("./insertableStreamWorker.js", { name: "Insertable Stream worker" });
insertableStreamWorker.onmessage = event => {
if (event.data === "insertableStreamWorkerLoaded") {
if (session.encodedInsertableStreams == "e2ee") {
if (session.password) {
insertableStreamWorker.postMessage({ cryptoPhrase: session.password + session.salt + "aDdedSaLt123" }); // salt ⚔️ rainbow
} else {
insertableStreamWorker.postMessage({ cryptoKey: "aabbccddeeff00112233445566778899" });
}
} else if (session.encodedInsertableStreams == "lyra") {
insertableStreamWorker.postMessage({
lyraCodecModule: session.lyraCodecModule
});
}
} else {
console.log(event.data);
}
};
}
try {
let operation = "pass";
if (session.encodedInsertableStreams == "e2ee") {
operation = "decode";
} else if (session.encodedInsertableStreams == "red") {
operation = "reddecode";
} else if (session.encodedInsertableStreams == "lyra" && receiver.track && receiver.track.kind === "audio") {
operation = "lyradecode";
}
const receiverStreams = receiver.createEncodedStreams();
const { readable, writable } = receiverStreams;
insertableStreamWorker.postMessage(
{
operation: operation,
readable,
writable
},
[readable, writable]
);
} catch (e) {
errorlog(e);
}
}
function mainMeshMask() {
if (session.TFJSModel === null || session.TFJSModel === true) {
setTimeout(function () {
mainMeshMask();
}, 1000);
return;
}
function heatMapColorforValue(value) {
var h = parseInt((1.0 - value) * 240);
if (h < 0) {
h = 0;
}
if (h > 240) {
h = 240;
}
return "hsl(" + h + ", 100%, 50%)";
}
async function process() {
if (session.TFJSModel.activelyProcessing) {
return;
}
session.TFJSModel.activelyProcessing = true;
if (session.effect !== "6") {
if (session.TFJSModel.timeoutDraw) {
session.TFJSModel.timeoutDraw();
session.TFJSModel.timeoutDraw = null;
}
session.TFJSModel.activelyProcessing = false;
return;
}
const predictions = await session.TFJSModel.estimateFaces({
input: session.canvasSource
});
var output = [];
if (predictions.length > 0) {
for (let j = 0; j < predictions.length; j++) {
const fp = predictions[j].annotations;
session.canvasCtx.fillStyle = "#000000";
session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height);
const keypoints = predictions[j].scaledMesh;
for (let i = 0; i < keypoints.length; i++) {
var [x, y, z] = keypoints[i];
x = parseInt(x);
y = parseInt(y);
z = parseInt(z);
if (session.pushEffectsData) {
output.push(x);
output.push(y);
}
session.canvasCtx.fillStyle = heatMapColorforValue((z + 40) / 60);
session.canvasCtx.fillRect(x, y, 5, 5);
}
}
}
if (session.pushEffectsData) {
//output = FastIntegerCompression.compress(output);
//log(output);
if (isIFrame) {
parent.postMessage(
{
effectsData: output,
eID: session.pushEffectsData
},
session.iframetarget
);
} else {
for (var i in session.pcs) {
if (!session.pcs[i].sendChannel.bufferedAmount) {
// don't overload things.
session.sendMessage({ effectsData: output, eID: session.effect }, i);
}
}
}
}
if (!session.TFJSModel.timeoutDraw) {
try {
session.TFJSModel.timeoutDraw = setupOscillator(process, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
session.TFJSModel.timeoutDraw = setupOscillator(process, 30); // setTimeout(function(){draw();},33);
}
}
session.TFJSModel.activelyProcessing = false;
}
process();
}
var faceDetector = false;
var faceAlignment = false;
var activeDetection = false;
function drawFace() {
if (session.effect !== "1") {
return;
}
if (faceAlignment) {
faceAlignment();
return;
} else if (faceAlignment === null) {
return;
}
faceAlignment = null;
var timers = {};
timers.activelyProcessingDraw = false;
var ctx = session.canvasCtx;
function fde1() {
warnlog("LOADED drawFace()");
var lastFace = {};
//session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
//session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
lastFace.x = session.canvasSource.width / 2;
lastFace.y = session.canvasSource.height / 2;
lastFace.w = session.canvasSource.width;
lastFace.h = session.canvasSource.height;
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
function detectFace() {
if (activeDetection) {
return;
}
activeDetection = true;
if (session.effect !== "1") {
return;
}
try {
faceDetector
.detect(session.canvasSource)
.then(faces => {
if (faces.length) {
for (let face of faces) {
lastFace.x = face.boundingBox.x;
lastFace.y = face.boundingBox.y;
lastFace.w = face.boundingBox.width;
lastFace.h = face.boundingBox.height;
break;
}
}
//setTimeout(function(){draw();},0);
})
.catch(e => {
errorlog("Boo, Face Detection failed: " + e);
});
} catch (e) {}
setTimeout(function () {
detectFace();
}, 200);
activeDetection = false;
}
var wh = null;
var xa = null;
var ya = null;
function draw() {
if (timers.activelyProcessingDraw) {
return;
}
timers.activelyProcessingDraw = true;
if (session.effect !== "1") {
timers.activelyProcessingDraw = false;
if (timers.timeoutDraw) {
timers.timeoutDraw();
timers.timeoutDraw = null;
}
return;
}
try {
if (!session.canvasSource.width) {
timers.activelyProcessingDraw = false;
return;
}
if (wh === null && session.canvasSource.width) {
wh = Math.pow((session.canvasSource.width * session.canvasSource.width) / 36, 0.5);
xa = 0;
ya = 0;
}
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
if (lastFace.w) {
wh = wh * 0.999 + Math.pow(lastFace.w * lastFace.h, 0.5) * 0.001;
var w = wh * 6;
if (w > session.canvasSource.width) {
w = session.canvasSource.width;
}
if (session.canvasSource.height > session.canvasSource.width) {
if (w > session.canvasSource.height && session.canvasSource.height > session.canvasSource.width) {
w = session.canvasSource.height;
}
} else if (w > session.canvasSource.width) {
w = session.canvasSource.width;
}
var h = (w / session.canvasSource.width) * session.canvasSource.height;
xa = xa * 0.998 + 0.002 * (lastFace.x + lastFace.w / 2);
ya = ya * 0.998 + 0.002 * (lastFace.y + lastFace.h / 2);
var x = xa - w / 2;
var y = ya - h / 2;
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (x > session.canvasSource.width - w) {
x = session.canvasSource.width - w;
}
if (y > session.canvasSource.height - h) {
y = session.canvasSource.height - h;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
}
//console.log(x, y, w, h, session.canvasSource.width, session.canvasSource.height);
ctx.drawImage(session.canvasSource, x, y, w, h, 0, 0, session.canvasSource.width, session.canvasSource.height);
//ctx.beginPath();
//ctx.rect(lastFace.x, lastFace.y, lastFace.w, lastFace.h);
// ctx.stroke();
} catch (e) {}
if (!timers.timeoutDraw) {
try {
timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
}
} else {
var res = timers.timeoutDraw("check");
if (res) {
try {
timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
}
}
}
timers.activelyProcessingDraw = false;
}
if (window.FaceDetector == undefined) {
if (!session.cleanOutput) {
warnUser("Face Detection API not detected.\n\nYou may be able to enable it here: chrome://flags/#enable-experimental-web-platform-features");
}
faceDetector = false;
} else {
faceDetector = new FaceDetector();
}
function fde2() {
if (!timers.activelyProcessingDraw) {
draw();
}
if (!activeDetection) {
detectFace();
}
}
fde2();
return fde2;
}
faceAlignment = fde1();
}
//////// END CANVAS EFFECTS ///////////////////
var getFacesActive = false;
async function getFaces() {
if (getFacesActive) {
return;
}
getFacesActive = true;
if (session.grabFaceData) {
if (!faceDetector) {
if (window.FaceDetector == undefined) {
if (!session.cleanOutput) {
warnUser("Face Detection API not detected.\n\nYou may be able to enable it here: chrome://flags/#enable-experimental-web-platform-features");
}
session.grabFaceData = false;
faceDetector = false;
getFacesActive = false;
return;
} else {
session.grabFaceData = 1;
faceDetector = new FaceDetector();
}
}
try {
var videos = {};
for (var UUID in session.rpcs) {
if (session.rpcs[UUID].videoElement) {
await faceDetector
.detect(session.rpcs[UUID].videoElement)
.then(faces => {
videos[session.rpcs[UUID].streamID] = {};
videos[session.rpcs[UUID].streamID].videoWidth = session.rpcs[UUID].videoElement.videoWidth;
videos[session.rpcs[UUID].streamID].videoHeight = session.rpcs[UUID].videoElement.videoHeight;
videos[session.rpcs[UUID].streamID].faces = faces;
})
.catch(e => {
//errorlog("Boo, Face Detection failed: " + e);
});
}
}
log(videos);
} catch (e) {}
pokeIframeAPI("face-tracking-data", videos);
setTimeout(function () {
getFaces();
}, 200);
}
getFacesActive = false;
}
//////
var simpleDrawMain = false;
function simpleDraw(reinit = false) {
let supported = ["8", "overlay"];
if (!supported.includes(session.effect)) {
errorlog("not a valid effeect?");
return;
}
if (simpleDrawMain) {
simpleDrawMain(reinit);
return;
} else if (simpleDrawMain === null) {
return;
}
simpleDrawMain = null;
var timers = {};
timers.activelyProcessingDraw = false;
function fde1() {
try {
log("LOADED simpleDraw()");
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
function draw() {
if (timers.activelyProcessingDraw) {
return;
}
timers.activelyProcessingDraw = true;
if (!supported.includes(session.effect)) {
if (timers.timeoutDraw) {
timers.timeoutDraw();
timers.timeoutDraw = null;
}
timers.activelyProcessingDraw = false;
return;
}
try {
if (!session.canvasSource.width) {
timers.activelyProcessingDraw = false;
return;
}
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, session.canvasSource.width, session.canvasSource.height);
if (session.effect === "overlay" && session.foregroundImg && session.foregroundImg.complete) {
session.canvasCtx.drawImage(session.foregroundImg, 0, 0, session.canvas.width, session.canvas.height);
}
} catch (e) {
errorlog(e);
}
if (!timers.timeoutDraw) {
try {
timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
}
} else {
var res = timers.timeoutDraw("check");
if (res) {
try {
timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
}
}
}
timers.activelyProcessingDraw = false;
}
} catch (e) {
errorlog(e);
timers.activelyProcessingDraw = false;
}
function fde2(reinit = false) {
if (reinit) {
if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) {
session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
}
}
if (!timers.activelyProcessingDraw) {
draw();
}
}
fde2();
return fde2;
}
simpleDrawMain = fde1();
}
//////// END CANVAS EFFECTS ///////////////////
//////
function changeZoomPosition(event, ele) {
const value = parseFloat(ele.value);
// Determine which slider was moved and update its pair
if (ele.id === "zoomPositionX1" || ele.id === "zoomPositionX") {
xPosition = value;
// Update other horizontal slider
const otherSlider = ele.id === "zoomPositionX1" ?
getById("zoomPositionX") :
getById("zoomPositionX1");
if (otherSlider) {
otherSlider.value = value;
}
} else if (ele.id === "zoomPositionY1" || ele.id === "zoomPositionY") {
yPosition = value;
// Update other vertical slider
const otherSlider = ele.id === "zoomPositionY1" ?
getById("zoomPositionY") :
getById("zoomPositionY1");
if (otherSlider) {
otherSlider.value = value;
}
}
}
var xPosition = 0.5; // Center position horizontally (0 to 1)
var yPosition = 0.5; // Center position vertically (0 to 1)
var digitalZoomMain = false;
function digitalZoom(resetZoom = false) {
if (session.effect !== "7") {
return;
}
if (digitalZoomMain) {
digitalZoomMain(resetZoom);
return;
} else if (digitalZoomMain === null) {
return;
}
digitalZoomMain = null;
var activelyProcessingDraw = false;
function fde1() {
try {
warnlog("LOADED digitalZoom()");
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
var xa = null;
var ya = null;
var zz = 1;
// Modify the draw function to use the position values
function draw() {
if (activelyProcessingDraw) {
return;
}
activelyProcessingDraw = true;
if (session.effect !== "7") {
zz = 1.0;
xa = 0;
ya = 0;
activelyProcessingDraw = false;
return;
}
try {
if (!session.canvasSource || !session.canvasSource.width) {
activelyProcessingDraw = false;
return;
}
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
if (session.effectValue) {
// Smooth out the zoom factor
zz = 0.9 * zz + session.effectValue * 0.1;
// Calculate the scaled dimensions
const scaledWidth = session.canvasSource.width / zz;
const scaledHeight = session.canvasSource.height / zz;
// Calculate the offset based on position sliders
// This centers the zoom on the selected position
xa = (session.canvasSource.width - scaledWidth) * xPosition;
ya = (session.canvasSource.height - scaledHeight) * yPosition;
// Draw the zoomed region
session.canvasCtx.drawImage(
session.canvasSource,
xa, ya,
scaledWidth,
scaledHeight,
0, 0,
session.canvasSource.width,
session.canvasSource.height
);
} else {
// If no zoom, draw the full image
session.canvasCtx.drawImage(
session.canvasSource,
0, 0,
session.canvasSource.width,
session.canvasSource.height,
0, 0,
session.canvasSource.width,
session.canvasSource.height
);
}
} catch (e) {
errorlog(e);
}
activelyProcessingDraw = false;
}
} catch (e) {
errorlog(e);
activelyProcessingDraw = false;
}
function fde2(resetZoom = false) {
if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) {
session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
}
if (resetZoom) {
xa = null;
ya = null;
zz = 1;
}
try {
setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
setupOscillator(draw, 40); // setTimeout(function(){draw();},33);
}
draw();
}
fde2();
return fde2;
}
digitalZoomMain = fde1();
digitalZoomMain(resetZoom);
}
function rgbToHsv(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, v = max;
const d = max - min;
s = max === 0 ? 0 : d / max;
if (max === min) {
h = 0;
} else {
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, v];
}
// real green scree filter
var chromaKeyMain = false;
function chromaKey(reinit = false) {
let supported = ["14", "15"];
if (!supported.includes(session.effect)) {
warnlog("not a valid effect");
return;
}
if (chromaKeyMain) {
chromaKeyMain(reinit);
return;
} else if (chromaKeyMain === null) {
return;
}
chromaKeyMain = null;
var timers = {};
timers.activelyProcessingDraw = false;
function fde1() {
try {
log("LOADED chromaKey()");
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
function processChromaKey() {
if (timers.activelyProcessingDraw) return;
timers.activelyProcessingDraw = true;
if (!supported.includes(session.effect)) {
if (timers.timeoutDraw) {
timers.timeoutDraw();
timers.timeoutDraw = null;
}
timers.activelyProcessingDraw = false;
return;
}
try {
if (!session.canvasSource.width) {
timers.activelyProcessingDraw = false;
return;
}
session.canvas.height = 2 * parseInt(session.canvasSource.height / 2);
session.canvas.width = 2 * parseInt(session.canvasSource.width / 2);
session.canvasCtx.drawImage(session.canvasSource, 0, 0);
const imageData = session.canvasCtx.getImageData(0, 0, session.canvas.width, session.canvas.height);
const data = imageData.data;
const threshold = parseInt(session.effectValue) || 80;
const smoothing = 15;
// Green range in HSV (normalized to 0-1)
const targetHue = 0.33; // Pure green
const hueRange = 0.15; // Range around pure green
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Quick pre-check for obvious non-green pixels
if (g <= r || g <= b) {
continue;
}
const [h, s, v] = rgbToHsv(r, g, b);
// Calculate how "green" the pixel is
const hueDiff = Math.abs(h - targetHue);
const isInGreenRange = hueDiff <= hueRange || hueDiff >= (1 - hueRange);
if (!isInGreenRange) {
continue;
}
// Green intensity calculation
const greenDominance = (g - Math.max(r, b)) / 255;
const saturationBoost = s * 0.7; // Reduce impact of washed-out greens
const keyValue = (greenDominance * 0.6 + saturationBoost * 0.4) * 100;
let alpha = 255;
if (keyValue > threshold) {
const smoothFactor = Math.min((keyValue - threshold) / smoothing, 1);
alpha = (1 - smoothFactor) * 255;
}
data[i + 3] = alpha;
}
session.canvasCtx.putImageData(imageData, 0, 0);
if (session.effect === "15" && session.effectsImage && session.effectsImage.complete) {
session.canvasCtx.globalCompositeOperation = 'destination-over';
session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height);
session.canvasCtx.globalCompositeOperation = 'source-over';
}
} catch (e) {
errorlog(e);
}
if (!timers.timeoutDraw) {
try {
timers.timeoutDraw = setupOscillator(processChromaKey, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
timers.timeoutDraw = setupOscillator(processChromaKey, 40);
}
} else {
var res = timers.timeoutDraw("check");
if (res) {
try {
timers.timeoutDraw = setupOscillator(processChromaKey, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30);
} catch (e) {
timers.timeoutDraw = setupOscillator(processChromaKey, 40);
}
}
}
timers.activelyProcessingDraw = false;
}
} catch (e) {
errorlog(e);
timers.activelyProcessingDraw = false;
}
function fde2(reinit = false) {
if (reinit) {
if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) {
session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280;
session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720;
}
}
if (!timers.activelyProcessingDraw) {
processChromaKey();
}
}
fde2();
return fde2;
}
chromaKeyMain = fde1();
}
//////// END CANVAS EFFECTS ///////////////////
function getNativeOutputResolution() {
try {
if (session.videoElement && session.videoElement.srcObject) {
var tracks = session.videoElement.srcObject.getVideoTracks();
if (tracks.length && tracks[0].getSettings) {
return tracks[0].getSettings();
} else {
return false;
}
} else if (session.videoElement && session.videoElement.videoWidth && session.videoElement.videoHeight) {
return { width: session.videoElement.videoWidth, height: session.videoElement.videoHeight };
}
} catch (e) {
return false;
}
}
function toggleSceneStats(button) {
var UUID = button.dataset.UUID;
if (button.value == 1) {
button.value = 0;
button.classList.remove("pressed");
button.ariaPressed = "false";
if (UUID) {
session.rpcs[UUID].allowGraphs = false;
} else {
session.allowDirectorGraph = false;
}
} else {
button.value = 1;
button.classList.add("pressed");
button.ariaPressed = "true";
if (UUID) {
session.rpcs[UUID].allowGraphs = true;
} else {
session.allowDirectorGraph = true;
}
}
if (UUID) {
var controls = getById("container_" + UUID);
} else {
var controls = getById("container_director");
}
if (button.value == 1) {
controls.querySelectorAll("[data-no-scenes]").forEach(ele => {
ele.classList.remove("hidden");
if (ele.dataset.message) {
ele.innerHTML = "Requesting data ..";
}
});
if (controls.querySelector('[data-action-type="stats-graphs-bitrate"]')) {
controls.querySelector('[data-action-type="stats-graphs-bitrate"]').classList.remove("hidden");
}
if (controls.querySelector('[data-action-type="stats-graphs-details"]')) {
controls.querySelector('[data-action-type="stats-graphs-details"]').classList.remove("hidden");
}
if (UUID) {
session.sendRequest({ requestStatsContinuous: true }, UUID);
}
} else {
if (UUID) {
session.sendRequest({ requestStatsContinuous: false }, UUID);
}
if (controls.querySelector('[data-action-type="stats-graphs-bitrate"]')) {
controls.querySelector('[data-action-type="stats-graphs-bitrate"]').classList.add("hidden");
}
if (controls.querySelector('[data-action-type="stats-graphs-details"]')) {
controls.querySelector('[data-action-type="stats-graphs-details"]').classList.add("hidden");
}
}
}
function getColor(value) {
var hue = (value * 120).toString(10);
return ["hsl(", hue, ",100%,50%)"].join("");
}
function plotData(info, UUID, uuid) {
// type = "bitrate" or "nacks"
log("plot data");
var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-bitrate"]');
if (!container) {
log("container not found");
return;
}
var canvas = getById("container_" + UUID).querySelector('canvas[data-uid="' + uuid + '"]');
var canvasNew = false;
if (!canvas) {
canvasNew = true;
canvas = document.createElement("canvas");
canvas.height = 50;
canvas.width = 124;
canvas.className = "canvasStats";
canvas.history_nacks = [];
canvas.history_bitrate = [];
canvas.target = 4000;
if (info.scene) {
canvas.title = "Scene: " + info.scene + ". Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments.";
} else if (info.label) {
canvas.title = "Label: " + info.label + ". Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments";
} else {
canvas.title = "Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments";
}
canvas.dataset.uid = uuid;
container.appendChild(canvas);
}
selfDestructElement(UUID, uuid);
var context = canvas.getContext("2d");
var bitrate = 0;
if ("video_bitrate_kbps" in info) {
bitrate = info.video_bitrate_kbps;
}
if (isNaN(bitrate)) {
bitrate = 0;
}
if (bitrate < 0) {
bitrate = 0;
}
var nacks = 0;
if ("nacks_per_second" in info) {
nacks = info.nacks_per_second;
}
if (isNaN(nacks)) {
nacks = 0;
}
if (nacks < 0) {
nacks = 0;
}
var height = context.canvas.height;
var width = context.canvas.width;
canvas.history_nacks.push(nacks);
canvas.history_bitrate.push(bitrate);
canvas.history_nacks = canvas.history_nacks.slice(-125);
canvas.history_bitrate = canvas.history_bitrate.slice(-125);
var maxBitrate = Math.max(...canvas.history_bitrate);
var target = canvas.target || 4000;
if (target && maxBitrate > target) {
canvas.target = maxBitrate * 1.5; // set it higher than it needs to be, so it doens't jump around a lot
var yScale = height / canvas.target;
context.clearRect(0, 0, width, height);
var x = width - 1;
var w = 1;
for (var i = 0; i < canvas.history_bitrate.length; i++) {
var nacks = canvas.history_nacks[i];
var bitrate = canvas.history_bitrate[i];
var val = (10 - nacks) / 10;
if (val > 1) {
val = 1;
} else if (val < 0) {
val = 0;
}
var color = getColor(val);
var y = height - bitrate * yScale;
context.fillStyle = color;
context.fillRect(x, y, w, height);
context.fillStyle = "#DDD5";
context.fillRect(x, y - 2, w, 4);
if (y - 5 > 0) {
context.fillStyle = "#FFF3";
context.fillRect(x, y + 2, w, 1);
}
var imageData = context.getImageData(1, 0, width - 1, height);
context.putImageData(imageData, 0, 0);
context.clearRect(width - 1, 0, 1, height);
}
for (var tt = 2500; tt < canvas.target; tt += 2500) {
var y = parseInt(height - tt * yScale);
context.fillStyle = "#0555";
context.fillRect(0, y, width, 1);
}
log("finished plotting a new y-axis");
return;
}
//if (info.available_outgoing_bitrate_kbps){
// limit target, but requires a history
//}
var val = (10 - nacks) / 10;
if (val > 1) {
val = 1;
} else if (val < 0) {
val = 0;
}
var color = getColor(val);
var yScale = height / target;
var x = width - 1;
var y = height - bitrate * yScale;
var w = 1;
context.fillStyle = color;
context.fillRect(x, y, w, height);
context.fillStyle = "#DDD5";
context.fillRect(x, y - 2, w, 4);
if (y - 5 > 0) {
context.fillStyle = "#FFF3";
context.fillRect(x, y + 2, w, 1);
}
context.fillStyle = "#0555";
if (canvasNew) {
for (var tt = 2500; tt < target; tt += 2500) {
var y = parseInt(height - tt * yScale);
context.fillRect(0, y, width, 1);
}
} else {
for (var tt = 2500; tt < target; tt += 2500) {
var y = parseInt(height - tt * yScale);
context.fillRect(x, y, 1, 1);
}
}
var imageData = context.getImageData(1, 0, width - 1, height);
context.putImageData(imageData, 0, 0);
context.clearRect(width - 1, 0, 1, height);
log("finished plotting");
}
function selfDestructElement(UUID, uid) {
getById("container_" + UUID)
.querySelectorAll('[data-uid="' + uid + '"]')
.forEach(ele => {
ele.classList.remove("greyout");
clearTimeout(ele.selfFadeout);
ele.selfFadeout = setTimeout(
function (ele) {
ele.classList.add("greyout");
},
4000,
ele
);
clearTimeout(ele.selfDestruct);
ele.selfDestruct = setTimeout(
function (ele) {
ele.remove();
},
10000,
ele
);
});
}
function directorGraphStats() {
if (!(session.allowDirectorGraph || session.allowGraphs)) {
return;
}
if (session.director) {
var UUID = "director";
var maincon = getById("container_director");
var sceneStats = {};
for (var uuid in session.pcs) {
if (session.pcs[uuid].scene !== false) {
sceneStats[uuid] = {};
sceneStats[uuid].label = session.pcs[uuid].label;
sceneStats[uuid].scene = session.pcs[uuid].scene;
sceneStats[uuid].resolution = session.pcs[uuid].stats.resolution;
sceneStats[uuid].video_bitrate_kbps = session.pcs[uuid].stats.video_bitrate_kbps;
sceneStats[uuid].video_encoder = session.pcs[uuid].stats.video_encoder;
}
}
if (!Object.keys(sceneStats).length) {
maincon.querySelectorAll("[data-no-scenes]").forEach(ele => {
ele.classList.remove("hidden");
if (ele.dataset.message) {
ele.innerHTML = "No scenes active";
}
});
log("zero size");
return;
}
maincon.querySelectorAll("[data-no-scenes]").forEach(ele => {
ele.classList.add("hidden");
});
for (var uuid in sceneStats) {
var container = maincon.querySelector('[data-action-type="stats-graphs-details-container"][data-uid="' + uuid + '"]');
if (!container) {
container = maincon.querySelector('[data-action-type="stats-graphs-details-container"]').cloneNode(true);
container.dataset.uid = uuid;
container.classList.remove("hidden");
maincon.querySelector('[data-action-type="stats-graphs-details"]').appendChild(container);
}
plotData(sceneStats[uuid], UUID, uuid);
if ("video_bitrate_kbps" in sceneStats[uuid] && sceneStats[uuid].video_bitrate_kbps !== "video_bitrate_kbps") {
var span = container.querySelector("[data-bitrate]");
if (span) {
span.classList.remove("hidden");
span.innerHTML = "video bitrate: " + parseInt(sceneStats[uuid].video_bitrate_kbps) + " (kbps)";
span.style.cursor = "pointer";
span.title = "Click to adjust bitrate";
span.onclick = async function(e) {
e.preventDefault();
e.stopPropagation();
var currentUUID = this.closest('[data-action-type="stats-graphs-details-container"]').dataset.uid;
const result = await promptAlt("Select target bitrate (kbps)", false, false, false, false, false, false, {
type: 'select',
options: ['50', '500', '1000', '2000', '5000', '10000', '20000', '[Custom]'],
placeholder: 'Enter custom bitrate in kbps'
});
if (result) {
var msg = {
targetBitrate: parseInt(result),
UUID: currentUUID,
requestAs: uuid
};
if (isIFrame) {
parent.postMessage(msg, session.iframetarget);
}
session.sendRequest(msg);
}
};
}
}
var span = container.querySelector("[data-scene-name]");
if (span && "label" in sceneStats[uuid] && sceneStats[uuid].label) {
span.classList.remove("hidden");
span.innerHTML = "stats for viewer: " + sceneStats[uuid].label;
} else if (span && "scene" in sceneStats[uuid] && sceneStats[uuid].scene !== false) {
span.classList.remove("hidden");
span.innerHTML = "stats for scene: " + sceneStats[uuid].scene;
} else if (uuid === "meshcast") {
span.classList.remove("hidden");
span.innerHTML = "stats for meshcast ingest";
span.title = "You can use &label=xxxx to give your view links a unique label";
} else {
span.classList.remove("hidden");
span.innerHTML = "stats for some viewer";
span.title = "You can use &label=xxxx to give your view links a unique label";
}
if ("resolution" in sceneStats[uuid]) {
var span = container.querySelector("[data-resolution]");
if (span) {
span.classList.remove("hidden");
span.innerHTML = sceneStats[uuid].resolution;
span.style.cursor = "pointer";
span.title = "Click to adjust resolution";
span.onclick = async function(e) {
e.preventDefault();
e.stopPropagation();
var currentUUID = this.closest('[data-action-type="stats-graphs-details-container"]').dataset.uid;
const result = await promptAlt("Select target resolution", false, false, false, false, false, false, {
type: 'select',
options: ['360', '720', '1080', '[Custom]'],
placeholder: 'Enter custom height in pixels'
});
if (result) {
session.requestResolution(currentUUID, 4096, result || 2160, false, uuid);
}
};
}
}
if ("video_encoder" in sceneStats[uuid]) {
var span = container.querySelector("[data-video-codec]");
if (span) {
span.classList.remove("hidden");
span.innerHTML = "video codec: " + sceneStats[uuid].video_encoder;
}
}
}
}
}
function remoteStats(msg, UUID) {
if (isIFrame) {
parent.postMessage({ remoteStats: msg.remoteStats, streamID: session.rpcs[UUID].streamID, UUID: UUID }, session.iframetarget);
}
if (!(session.rpcs[UUID].allowGraphs || session.allowGraphs)) {
return;
}
if (session.director) {
var size = 0;
for (var key in msg.remoteStats) {
if (msg.remoteStats.hasOwnProperty(key)) {
size++;
}
}
if (!size) {
getById("container_" + UUID)
.querySelectorAll("[data-no-scenes]")
.forEach(ele => {
ele.classList.remove("hidden");
if (ele.dataset.message) {
ele.innerHTML = "No scenes active";
}
});
log("zero size");
return;
}
getById("container_" + UUID)
.querySelectorAll("[data-no-scenes]")
.forEach(ele => {
ele.classList.add("hidden");
});
for (var uuid in msg.remoteStats) {
var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-details-container"][data-uid="' + uuid + '"]');
if (!container) {
container = getById("container_" + UUID)
.querySelector('[data-action-type="stats-graphs-details-container"]')
.cloneNode(true);
container.dataset.uid = uuid;
container.classList.remove("hidden");
getById("container_" + UUID)
.querySelector('[data-action-type="stats-graphs-details"]')
.appendChild(container);
}
plotData(msg.remoteStats[uuid], UUID, uuid);
if ("video_bitrate_kbps" in msg.remoteStats[uuid] && msg.remoteStats[uuid].video_bitrate_kbps !== "video_bitrate_kbps") {
var span = container.querySelector("[data-bitrate]");
if (span) {
span.classList.remove("hidden");
span.innerHTML = "video bitrate: " + parseInt(msg.remoteStats[uuid].video_bitrate_kbps) + " (kbps)";
span.style.cursor = "pointer";
span.title = "Click to adjust bitrate";
span.onclick = async function(e) {
e.preventDefault();
e.stopPropagation();
const result = await promptAlt("Select target bitrate (kbps)", false, false, false, false, false, false, {
type: 'select',
options: ['50', '500', '1000', '2000', '5000', '10000', '20000', '[Custom]'],
placeholder: 'Enter custom bitrate in kbps'
});
if (result) {
var msg = {
targetBitrate: parseInt(result),
UUID: UUID,
requestAs: uuid
};
if (isIFrame) {
parent.postMessage(msg, session.iframetarget);
}
session.sendRequest(msg);
}
};
}
}
var span = container.querySelector("[data-scene-name]");
if (span && "label" in msg.remoteStats[uuid] && msg.remoteStats[uuid].label) {
span.classList.remove("hidden");
span.innerHTML = "stats for viewer: " + msg.remoteStats[uuid].label;
} else if (span && "scene" in msg.remoteStats[uuid] && msg.remoteStats[uuid].scene !== false) {
span.classList.remove("hidden");
span.innerHTML = "stats for scene: " + msg.remoteStats[uuid].scene;
} else if (uuid === "meshcast") {
span.classList.remove("hidden");
span.innerHTML = "stats for meshcast ingest";
span.title = "You can use &label=xxxx to give your view links a unique label";
} else {
span.classList.remove("hidden");
span.innerHTML = "stats for some viewer";
span.title = "You can use &label=xxxx to give your view links a unique label";
}
if ("resolution" in msg.remoteStats[uuid]) {
var span = container.querySelector("[data-resolution]");
if (span) {
span.classList.remove("hidden");
span.innerHTML = msg.remoteStats[uuid].resolution;
span.style.cursor = "pointer";
span.title = "Click to adjust resolution";
span.onclick = async function(e) {
e.preventDefault();
e.stopPropagation();
const result = await promptAlt("Select target resolution", false, false, false, false, false, false, {
type: 'select',
options: ['360', '720', '1080', '1440', '2160', '[Custom]'],
placeholder: 'Enter custom height in pixels'
});
if (result) {
session.requestResolution(UUID, 4096, result || 2160, false, uuid);
}
};
}
}
if ("video_encoder" in msg.remoteStats[uuid]) {
var span = container.querySelector("[data-video-codec]");
if (span) {
span.classList.remove("hidden");
span.innerHTML = "video codec: " + msg.remoteStats[uuid].video_encoder;
}
}
}
}
}
function processStats(UUID) {
// for (pc in session.pcs){session.pcs[pc].getStats().then(function(stats) {stats.forEach(stat=>{if (stat.id.includes("RTCIce")){console.log(stat)}})})};
if (!session.rpcs || !(UUID in session.rpcs)) {
return;
}
try {
if (session.rpcs[UUID].videoElement.paused) {
if (session.firstPlayTriggered) {
if (session.audioCtx.state == "suspended") {
// added oct 9th 2022
try {
session.audioCtx.resume();
} catch (e) {
warnlog(e);
}
}
if (session.audioCtx.state == "running") {
// NOTE: I Don't know why this was
log("trying to play");
session.rpcs[UUID].videoElement
.play()
.then(_ => {
log("playing 8");
//if ((session.audioEffects===true) || session.pushLoudness){
// updateIncomingAudioElement(UUID);
//}
})
.catch(warnlog);
}
}
}
} catch (e) {}
try {
if (session.rpcs[UUID].realUUID && session.rpcs[session.rpcs[UUID].realUUID]) {
var node = session.rpcs[session.rpcs[UUID].realUUID];
} else {
var node = session.rpcs[UUID];
}
var validTrackIds = [];
if (session.rpcs[UUID].streamSrc) {
session.rpcs[UUID].streamSrc.getTracks().forEach(trk => {
validTrackIds.push(trk.id);
});
}
if (session.rpcs[UUID].whep) {
processMeshcastStats(UUID);
if (!node.getStats) {
clearTimeout(session.rpcs[UUID].getStatsTimeout);
session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, session.statsInterval, UUID);
//setTimeout(processStats, session.statsInterval, UUID); // no p2p, so lets do WHEP again manually.
}
}
if (node.getStats) {
node.getStats().then(function (stats) {
if (!(UUID in session.rpcs)) {
return;
}
clearTimeout(session.rpcs[UUID].getStatsTimeout);
session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, session.statsInterval, UUID);
if (!session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"] = {};
}
var nominatedCandidate = false;
var candidates = {};
var ipleakingAllowedRemote = false;
var ipleakingAllowedLocal = false;
stats.forEach(stat => {
try {
if (stat.id && stat.id.startsWith("DEPRECATED_")) {
return;
}
var trackID = stat.trackIdentifier || stat.id || false;
if (stat.type == "track" && stat.remoteSource) {
if (stat.trackIdentifier && !validTrackIds.includes(stat.trackIdentifier)) {
return;
}
if (stat.id in session.rpcs[UUID].stats) {
session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier;
session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0;
session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
if ("frameWidth" in stat) {
if ("frameHeight" in stat) {
session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth + " x " + stat.frameHeight;
session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth;
session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight;
}
}
} else {
var media = {};
media._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
media._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
media.Jitter_Buffer_ms = 0;
media._trackID = stat.trackIdentifier;
session.rpcs[UUID].stats[stat.id] = media;
if (stat.kind && stat.kind == "audio") {
session.rpcs[UUID].stats[stat.id].type = "Audio Track";
session.rpcs[UUID].stats[stat.id]._type = "audio";
} else if (stat.kind && stat.kind == "video") {
session.rpcs[UUID].stats[stat.id].type = "Video Track";
session.rpcs[UUID].stats[stat.id]._type = "video";
}
}
} else if (stat.type == "remote-candidate") {
candidates[stat.id] = stat;
if (stat.candidateType != "relay") {
ipleakingAllowedRemote = true;
}
} else if (stat.type == "local-candidate") {
candidates[stat.id] = stat;
if (stat.candidateType != "relay") {
ipleakingAllowedLocal = true;
}
} else if (stat.type == "candidate-pair" && stat.nominated) {
if (!nominatedCandidate) {
nominatedCandidate = stat;
} else if (nominatedCandidate.priority < stat.priority) {
nominatedCandidate = stat;
}
} else if (stat.type == "transport") {
if ("bytesReceived" in stat) {
if ("_bytesReceived" in session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) {
if (session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp) {
if (stat.timestamp) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].total_recv_bitrate_kbps = parseInt((8 * (stat.bytesReceived - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._bytesReceived)) / (stat.timestamp - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp));
hideStreamLowBandwidth(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].total_recv_bitrate_kbps, UUID);
//changeSceneLowBandwidth(session.rpcs[UUID].stats['Peer-to-Peer_Connection'].total_recv_bitrate_kbps, UUID);
}
}
}
session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._bytesReceived = stat.bytesReceived;
}
if ("timestamp" in stat) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp = stat.timestamp;
if (!session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart = stat.timestamp;
} else {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart) / 600) / 100;
}
}
} else if (stat.type == "inbound-rtp" && trackID) {
if (stat.trackIdentifier && !validTrackIds.includes(stat.trackIdentifier)) {
return;
}
session.rpcs[UUID].stats[trackID] = session.rpcs[UUID].stats[trackID] || {};
if (stat.trackIdentifier) {
session.rpcs[UUID].stats[trackID]._trackID = stat.trackIdentifier;
}
if ("jitterBufferDelay" in stat) {
session.rpcs[UUID].stats[trackID].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[trackID]._jitter_delay_2)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[trackID]._jitter_count_2)) || 0;
session.rpcs[UUID].stats[trackID]._jitter_delay_2 = parseFloat(stat.jitterBufferDelay) || 0;
session.rpcs[UUID].stats[trackID]._jitter_count_2 = parseInt(stat.jitterBufferEmittedCount) || 0;
}
if ("frameWidth" in stat) {
if ("frameHeight" in stat) {
session.rpcs[UUID].stats[trackID].Resolution = stat.frameWidth + " x " + stat.frameHeight;
session.rpcs[UUID].stats[trackID]._frameWidth = stat.frameWidth;
session.rpcs[UUID].stats[trackID]._frameHeight = stat.frameHeight;
}
}
session.rpcs[UUID].stats[trackID].Bitrate_in_kbps = parseInt((8 * (stat.bytesReceived - (session.rpcs[UUID].stats[trackID]._last_bytes || 0))) / (stat.timestamp - session.rpcs[UUID].stats[trackID]._last_time));
session.rpcs[UUID].stats[trackID]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[trackID]._last_bytes;
session.rpcs[UUID].stats[trackID]._last_time = stat.timestamp || session.rpcs[UUID].stats[trackID]._last_time;
if (stat.mediaType == "video") {
session.rpcs[UUID].stats._codecId = stat.codecId;
session.rpcs[UUID].stats._codecIdTrackId = trackID;
session.rpcs[UUID].stats[trackID].type = "Video Stream";
session.rpcs[UUID].stats[trackID]._type = "video";
if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP8") {
session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0;
session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0;
log("OBS PLI FIX MODE ON");
if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix) {
// heavy packet loss with no pliCount?
session.requestKeyframe(UUID);
session.rpcs[UUID].stats[trackID].nackTrigger = 0;
log("TRYING KEYFRAME");
} else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) {
session.rpcs[UUID].stats[trackID].nackTrigger = 0;
}
} else if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP9") {
session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0;
session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0;
log("OBS PLI FIX MODE ON");
if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix * 4) {
// heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often.
session.requestKeyframe(UUID);
session.rpcs[UUID].stats[trackID].nackTrigger = 0;
log("TRYING KEYFRAME");
} else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) {
session.rpcs[UUID].stats[trackID].nackTrigger = 0;
}
}
session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0;
session.rpcs[UUID].stats[trackID].streamErrors_nackCount = stat.nackCount || 0;
//warnlog(stat);
if ("framesPerSecond" in stat) {
session.rpcs[UUID].stats[trackID].FPS = parseInt(stat.framesPerSecond);
} else if ("framesDecoded" in stat && stat.timestamp) {
var lastFramesDecoded = 0;
var lastTimestamp = 0;
try {
lastFramesDecoded = session.rpcs[UUID].stats[trackID]._framesDecoded;
lastTimestamp = session.rpcs[UUID].stats[trackID]._timestamp;
} catch (e) {}
session.rpcs[UUID].stats[trackID].FPS = parseInt((10 * (stat.framesDecoded - lastFramesDecoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10;
//session.rpcs[UUID].stats[trackID].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp));
session.rpcs[UUID].stats[trackID]._framesDecoded = stat.framesDecoded;
session.rpcs[UUID].stats[trackID]._timestamp = stat.timestamp / 1000;
}
} else if (stat.mediaType == "audio") {
//log("AUDIO LEVEL: "+stat.audioLevel);
session.rpcs[UUID].stats._audioCodecId = stat.codecId;
session.rpcs[UUID].stats._audioCodecIdTrackId = trackID;
session.rpcs[UUID].stats[trackID].type = "Audio Stream";
session.rpcs[UUID].stats[trackID]._type = "audio";
if ("audioLevel" in stat) {
session.rpcs[UUID].stats[trackID].audio_level = parseInt(parseFloat(stat.audioLevel) * 10000) / 10000.0;
}
}
if ("packetsLost" in stat && "packetsReceived" in stat) {
if (!("_packetsLost" in session.rpcs[UUID].stats[trackID])) {
session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost;
}
if (!("_packetsReceived" in session.rpcs[UUID].stats[trackID])) {
session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived;
}
if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[trackID])) {
session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = 0;
}
let packetLoss = ((stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost) * 100.0) / (stat.packetsReceived - session.rpcs[UUID].stats[trackID]._packetsReceived + (stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost)) || 0;
/* if (session.rpcs[UUID].stats[trackID]._type && (session.rpcs[UUID].stats[trackID]._type =="video")){
if (packetLoss>1){
var data = {};
data.bitrate = parseInt(session.rpcs[UUID].stats[trackID].Bitrate_in_kbps*0.8);
session.sendRequest(data,UUID);
} else {
var data = {};
data.bitrate = parseInt(session.rpcs[UUID].stats[trackID].Bitrate_in_kbps*1.1);
session.sendRequest(data,UUID);
}
} */
session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage * 0.35 + 0.65 * packetLoss;
if (session.rpcs[UUID].signalMeter && session.rpcs[UUID].stats[trackID]._type === "video") {
if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.01) {
if (session.rpcs[UUID].stats[trackID].Bitrate_in_kbps == 0) {
session.rpcs[UUID].signalMeter.dataset.level = 0;
} else {
session.rpcs[UUID].signalMeter.dataset.level = 5;
}
} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.3) {
session.rpcs[UUID].signalMeter.dataset.level = 4;
} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 1.0) {
session.rpcs[UUID].signalMeter.dataset.level = 3;
} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 3.5) {
session.rpcs[UUID].signalMeter.dataset.level = 2;
} else {
session.rpcs[UUID].signalMeter.dataset.level = 1;
}
}
session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived;
session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost;
}
} else if ("_codecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._codecId) {
if ("mimeType" in stat) {
if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]) {
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
} else {
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {};
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
}
}
if ("frameHeight" in stat) {
if ("frameWidth" in stat) {
session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth) + " x " + parseInt(stat.frameHeight);
}
}
} else if ("_audioCodecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._audioCodecId) {
if ("mimeType" in stat) {
var addOnDescription = stat.mimeType;
addOnDescription = addOnDescription.replace("audio/", "");
if ("sdpFmtpLine" in stat) {
if (stat.sdpFmtpLine.includes("useinbandfec=1")) {
addOnDescription += ", /w fec";
}
}
if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId]) {
} else {
session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId] = {};
}
session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].codec = addOnDescription;
if (stat.clockRate) {
session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].clockRate = stat.clockRate;
if (stat.channels) {
session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].clockRate += " / " + stat.channels;
}
}
}
} else if (Firefox) {
if ("frameWidth" in stat) {
session.rpcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight;
if ("framesPerSecond" in stat) {
session.rpcs[UUID].stats.resolution += " @ " + stat.framesPerSecond;
}
}
if ("mimeType" in stat && "type" in stat && "id" in stat && stat.type == "codec") {
if (stat.mimeType.includes("video")) {
session.rpcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1];
} else if (stat.mimeType.includes("audio")) {
session.rpcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1];
if (stat.clockRate) {
session.rpcs[UUID].stats.audio_clockRate = stat.clockRate;
if (stat.channels) {
session.rpcs[UUID].stats.audio_clockRate += " / " + stat.channels;
}
} else if (stat.sdpFmtpLine) {
session.rpcs[UUID].stats.fmtp = stat.sdpFmtpLine;
}
}
}
/* if ("jitter" in stat){
if (("kind" in stat) && (stat.kind=="video")){
session.rpcs[UUID].stats.video_jitter_ms = parseInt(stat.jitter*1000);
} else if (("kind" in stat) && (stat.kind=="audio")){
session.rpcs[UUID].stats.audio_jitter_ms = parseInt(stat.jitter*1000);
}
} */
if ("bytesReceived" in stat) {
if ("kind" in stat && stat.kind == "video") {
if ("_bytesReceived_video" in session.rpcs[UUID].stats) {
session.rpcs[UUID].stats.videoBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_video) / ((1024 * session.statsInterval) / 8000));
}
session.rpcs[UUID].stats._bytesReceived_video = stat.bytesReceived;
} else if ("kind" in stat && stat.kind == "audio") {
if ("_bytesReceived_audio" in session.rpcs[UUID].stats) {
session.rpcs[UUID].stats.audioBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_audio) / ((1024 * session.statsInterval) / 8000));
}
session.rpcs[UUID].stats._bytesReceived_audio = stat.bytesReceived;
}
}
}
} catch (e) {
errorlog(e);
}
});
//////////
if (nominatedCandidate) {
if (nominatedCandidate.localCandidateId && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id !== nominatedCandidate.localCandidateId) {
if ("candidateType" in nominatedCandidate) {
try {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_local;
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP;
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol;
} catch (e) {}
}
}
if (nominatedCandidate.remoteCandidateId && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id !== nominatedCandidate.remoteCandidateId) {
if ("candidateType" in nominatedCandidate) {
try {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_remote;
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP;
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol;
} catch (e) {}
}
}
if (nominatedCandidate.localCandidateId) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id = nominatedCandidate.localCandidateId;
}
if (nominatedCandidate.remoteCandidateId) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id = nominatedCandidate.remoteCandidateId;
}
if ("currentRoundTripTime" in nominatedCandidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime * 1000;
}
}
if (nominatedCandidate && nominatedCandidate.localCandidateId) {
if (candidates[nominatedCandidate.localCandidateId]) {
var candidate = candidates[nominatedCandidate.localCandidateId];
if ("candidateType" in candidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_local = candidate.candidateType;
if (candidate.candidateType === "relay") {
if ("relayProtocol" in candidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol = candidate.relayProtocol;
} else {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol;
}
if ("ip" in candidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP = candidate.ip;
} else {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP;
}
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_ip_blocking = !ipleakingAllowedLocal;
} else {
try {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP;
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol;
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_ip_blocking;
} catch (e) {}
}
}
if ("networkType" in candidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_networkType = candidate.networkType;
}
}
}
if (nominatedCandidate && nominatedCandidate.remoteCandidateId) {
if (candidates[nominatedCandidate.remoteCandidateId]) {
var candidate = candidates[nominatedCandidate.remoteCandidateId];
if ("candidateType" in candidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_remote = candidate.candidateType;
if (candidate.candidateType === "relay") {
if ("ip" in candidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP = candidate.ip;
} else {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP;
}
if ("relayProtocol" in candidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol = candidate.relayProtocol;
} else {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol;
}
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_ip_blocking = !ipleakingAllowedRemote;
} else {
try {
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP;
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol;
delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_ip_blocking;
} catch (e) {}
}
}
if ("networkType" in candidate) {
session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_networkType = candidate.networkType;
}
}
}
playoutdelay(UUID);
setTimeout(function () {
session.directorSpeakerMute();
session.directorDisplayMute();
}, 0);
});
}
} catch (e) {
errorlog(e);
}
pokeIframeAPI("view-stats-updated", true, UUID);
}
function createConnectionDetailsEle(UUID) {
if (!session.rpcs[UUID]) {
return false;
}
session.rpcs[UUID].connectionDetails = document.createElement("div");
session.rpcs[UUID].connectionDetails.id = "remoteConnections_" + UUID;
if (session.rpcs[UUID].stats.info && "total_outbound_p2p_connections" in session.rpcs[UUID].stats.info) {
session.rpcs[UUID].connectionDetails.innerText = "🔗" + session.rpcs[UUID].stats.info.total_outbound_p2p_connections;
session.rpcs[UUID].connectionDetails.dataset.value = session.rpcs[UUID].stats.info.total_outbound_p2p_connections;
}
session.rpcs[UUID].connectionDetails.dataset.UUID = UUID;
session.rpcs[UUID].connectionDetails.title = getTranslation("viewer-count");
session.rpcs[UUID].connectionDetails.className = "rem-con-count";
session.rpcs[UUID].connectionDetails.addEventListener("click", function (e) {
// show stats of video if double clicked
log("clicked connectionDetails icon ");
try {
e.preventDefault();
if (session.statsMenu !== false) {
var uid = e.currentTarget.dataset.UUID;
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
}
e.stopPropagation();
return false;
} catch (e) {
errorlog(e);
}
});
return true;
}
function playoutdelay(UUID) {
// applies a delay to all videos
try {
if ((session.rpcs[UUID].buffer !== false) || (session.buffer!=false) || (session.audioBuffer!==false)) {
// if buffer is set, then session.sync will be set; at least to 0.
var receivers = getReceivers2(UUID).reverse() || []; //session.rpcs[UUID].getReceivers().reverse();
if (session.rpcs[UUID].whep) {
receivers = receivers.concat(getReceiversMC(UUID).reverse()); // if I try to reuse getReceivers2, I get some confused stats (not able to tell tracks apart) TODO: see if this issue is a problem else where, esp with screen shares. sstype==3
}
receivers.forEach(function (receiver) {
try {
for (var tid in session.rpcs[UUID].stats) {
if (typeof session.rpcs[UUID].stats[tid] == "object" && "_trackID" in session.rpcs[UUID].stats[tid] && session.rpcs[UUID].stats[tid]._trackID === receiver.track.id && session.rpcs[UUID].stats[tid]._type == receiver.track.kind && "Jitter_Buffer_ms" in session.rpcs[UUID].stats[tid]) {
//if (ChromiumVersion<=103){ // I don't know the exact version, except I know OBS Studio is 103 and it uses the old way still.netwqor
var sync_offset = 0.0;
if (session.rpcs[UUID].stats[tid]._sync_offset) {
sync_offset = session.rpcs[UUID].stats[tid]._sync_offset || 0;
} else {
session.rpcs[UUID].stats[tid]._sync_offset = 0;
}
var target_buffer = 0;
if (session.rpcs[UUID].stats[tid]._type == "audio") {
if (session.audioBuffer!==false){
target_buffer = parseFloat(session.audioBuffer || 0);
} else {
target_buffer = parseFloat(session.buffer || 0);
}
} else {
target_buffer = parseFloat(session.buffer || 0);
}
if (session.rpcs[UUID].buffer !== false){
target_buffer = parseFloat(session.rpcs[UUID].buffer);
}
sync_offset += target_buffer;
sync_offset -= session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0;
if (session.includeRTT && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) {
sync_offset -= parseInt(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms / 2) || 0; // I can't be sure what the actual one-way delay is
}
if (sync_offset > target_buffer) {
sync_offset = target_buffer || 0;
}
if (sync_offset < 0) {
sync_offset = 0;
}
session.rpcs[UUID].stats[tid].Added_Buffer_Delay_ms = sync_offset || 0;
if (session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) {
session.rpcs[UUID].stats[tid].Total_Playout_Delay_ms = sync_offset + parseInt(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms / 2) + session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0;
}
if (session.rpcs[UUID].stats[tid]._type == "audio") {
session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0;
if ("jitterBufferTarget" in receiver){
receiver.jitterBufferTarget = parseFloat(sync_offset) || 0;
} else {
receiver.playoutDelayHint = parseFloat(sync_offset / 1000) || 0;
}
// receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe
if (session.sync !== false) {
var audio_delay = session.sync || 0; // video is typically showing greater delay than audio.
audio_delay += target_buffer - session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0;
if (receiver.track.kind == "audio" && receiver.track.id in session.rpcs[UUID].inboundAudioPipeline) {
if (session.rpcs[UUID].inboundAudioPipeline[receiver.track.id] && session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode) {
if (audio_delay < 0) {
audio_delay = 0;
}
try {
session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.linearRampToValueAtTime(parseFloat(audio_delay / 1000.0), session.audioCtx.currentTime + parseFloat(session.statsInterval / 9000));
} catch (e) {
session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.setValueAtTime(parseFloat(audio_delay / 1000.0), session.audioCtx.currentTime + 1);
}
session.rpcs[UUID].stats[tid].Audio_Sync_Delay_ms = audio_delay || 0;
}
}
}
} else if (session.rpcs[UUID].stats[tid]._type == "video") {
session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0;
if ("jitterBufferTarget" in receiver){
receiver.jitterBufferTarget = parseFloat(sync_offset) || 0;
} else {
receiver.playoutDelayHint = parseFloat(sync_offset / 1000) || 0;
}
// receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe
}
}
}
} catch (e) {
errorlog(e);
}
});
}
} catch (e) {
errorlog(e);
warnlog("device does not support playout delay");
}
}
function printViewStats(menu, UUID) {
// Stats for viewing a remote video
if (session.statsMenu === false) {
return false;
}
if (!session.rpcs[UUID]) {
menu.innerHTML = "
Remote Publisher Disconnected";
return false;
}
var statsObj = session.rpcs[UUID].stats;
var streamID = session.rpcs[UUID].streamID;
var scrollLeft = menu.scrollLeft;
var scrollTop = menu.scrollTop;
menu.innerHTML = "StreamID: " + streamID + " ";
//// doesn't work on viewer side.
//if (session.rpcs && session.rpcs[UUID] && session.rpcs[UUID] && session.rpcs[UUID].restartIce){ // only show if available
// menu.innerHTML += "";
//}
menu.innerHTML += printValues(statsObj);
menu.scrollTop = scrollTop;
menu.scrollLeft = scrollLeft;
return true;
}
function plotDataSimple(canvas, bitrate, nacks = 0) {
canvas.height = 50;
canvas.width = 124;
canvas.className = "canvasStats";
var context = canvas.getContext("2d");
if (isNaN(bitrate)) {
bitrate = 0;
}
if (isNaN(nacks)) {
nacks = 0;
}
var height = context.canvas.height;
var width = context.canvas.width;
var val = (10 - nacks) / 10;
if (val > 1) {
val = 1;
} else if (val < 0) {
val = 0;
}
var yScale = height / 4000;
var x = width - 1;
var y = height - bitrate * yScale;
var w = 1;
context.fillStyle = getColor(val);
context.fillRect(x, y, w, height);
context.fillStyle = "#FFFFFF55";
context.fillRect(x, y - 2, w, 4);
if (y - 5 > 0) {
context.fillStyle = "#FFFFFF44";
context.fillRect(x, y + 2, w, 1);
}
context.putImageData(context.getImageData(1, 0, width - 1, height), 0, 0);
context.clearRect(width - 1, 0, 1, height);
}
function printValues(obj, sort = false) {
// see: printViewStats
var out = "";
var keys = Object.keys(obj);
if (sort) {
keys.sort();
}
var lat = false;
var lon = false;
keys.forEach(key => {
if (key.startsWith("_")) return;
if (typeof obj[key] === "object" && obj[key] !== null) {
let tmp = sanitizeChat(key);
out += `
${tmp}
`;
if (key == "info") {
out += printValues(obj[key]);
} else if (key == "meta") {
out += `
`;
Object.entries(obj[key]).forEach(([category, data]) => {
if (data.type === "file" && data.filetype && data.filetype.startsWith("image/")) {
out += `
`;
} else {
out += printValues(obj[key], true);
}
} else {
try {
var unit = "";
var value = obj[key];
var stat = sanitizeChat(key);
stat = stat.charAt(0).toUpperCase() + stat.slice(1);
var hint = "";
if (typeof obj[key] == "string") {
value = sanitizeChat(value);
}
if (key == "useragent") {
value = "" + value + "";
}
if (key == "Bitrate_in_kbps") {
var unit = " kbps";
stat = "Bitrate";
hint = "You can refer to the documentation for ways to increase the target bitrate";
} else if (key == "type") {
var unit = "";
stat = "Type";
if (value == "Audio Track") {
value = "🔊 " + value;
//out += "";
}
if (value == "Video Track") {
value = "📺 " + value;
}
} else if (key == "packetLoss_in_percentage") {
var unit = " %";
stat = "Packet Loss 📶";
value = parseInt(parseFloat(value) * 10000) / 10000.0;
hint = "A high packet loss will lower quality of the media";
} else if (key == "local_relay_IP") {
value = "" + value + "";
} else if (key == "remote_relay_IP") {
value = "" + value + "";
} else if (key == "local_ip_blocking" && value) {
console.warn("Your system or connection is blocking p2p traffic");
value = "⚠️ You're blocking";
hint = "no direct p2p connection made because of YOUR browser or system setting";
} else if (key == "remote_ip_blocking" && value) {
console.warn("A remote client is blocking p2p traffic");
value = "⚠️ They're blocking";
hint = "no direct p2p connection made because of THEIR browser or system setting";
} else if (key == "candidateType_local" && value == "relay") {
value = "💸 relay server";
hint = "no direct p2p connection made; using the TURN relay servers.";
stat = "Candidate type - Local";
} else if (key == "candidateType_remote" && value == "relay") {
value = "💸 relay server";
hint = "no direct p2p connection made; using the TURN relay servers.";
stat = "Candidate type - Remote";
} else if (key == "candidateType_local" && value == "host") {
hint = "No NAT firewall, typical of LAN to LAN";
stat = "Candidate type - Local";
} else if (key == "candidateType_remote" && value == "host") {
hint = "No NAT firewall, typical of LAN to LAN";
stat = "Candidate type - Remote";
} else if (key == "candidateType_local" && value == "srflx") {
hint = "direct p2p, but NAT firewall likely";
stat = "Candidate type - Local";
} else if (key == "candidateType_remote" && value == "srflx") {
hint = "direct p2p, but NAT firewall likely";
stat = "Candidate type - Remote";
} else if (key == "height_url") {
if (value == false) {
return;
}
} else if (key == "width_url") {
if (value == false) {
return;
}
} else if (key == "height_url") {
if (value == false) {
return;
}
} else if (key == "version") {
stat = "VDO.Ninja Version";
} else if (key == "platform") {
stat = "Platform (OS)";
} else if (key == "iPhone12Up") {
stat = "iPhone 12 and up";
} else if (key == "aec_url") {
stat = "Echo-Cancellation";
} else if (key == "agc_url") {
stat = "Auto-Gain (agc)";
} else if (key == "denoise_url") {
stat = "De-noising ";
} else if (key == "audio_level") {
stat = "Audio Level";
} else if (key == "Jitter_Buffer_ms") {
var unit = " ms";
stat = "Jitter Buffer Delay";
} else if (key == "Added_Buffer_Delay_ms") {
var unit = " ms";
stat = "Added Buffer Delay";
hint = "Value of playout buffer delay added if using &buffer";
} else if (key == "Total_Playout_Delay_ms") {
// doesn't include bluetooth / monitor / capture delay, etc.
var unit = " ms";
stat = "Total Playout Delay";
hint = "Network latency + Jitter buffer + any manually added playout delay";
} else if (value === null) {
value = "null";
} else if (key == "stereo_url") {
stat = "Pro-Audio (Stereo-mode)";
if (value == 3) {
value = "3 (outbound hi-fi) Use Headphones";
} else if (value == 1) {
value = "1 (in & out hi-fi) Use Headphones";
} else if (value == 2) {
value = "3 (inbound hi-fi)";
} else if (value == 4) {
value = "3 (multichannel) Use Headphones";
} else if (value == 5) {
value = "5 (auto-mode) Use Headphones";
}
} else if (value === false) {
return;
} else if (value === "false") {
return;
} else if (key == "lat") {
lat = value;
if (lat && lon) {
const mapWidth = 250;
const mapHeight = 250;
const x = (lon + 180) * (mapWidth / 360);
const mapRatio = mapHeight / mapWidth;
const latRad = (lat * Math.PI) / 180;
const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2));
const y = mapHeight / 2 - ((mapWidth * mercN) / (2 * Math.PI)) * mapRatio;
out +=
'
';
out += 'Open Location in Google Maps';
}
}
stat = stat.replaceAll("_", " ");
stat = stat.trim();
if (hint) {
out += "
" + stat + "" + value + unit + "
";
} else {
out += "
" + stat + "" + value + unit + "
";
}
} catch (e) {
warnlog(e);
}
}
});
return out;
}
function processMeshcastStats(UUID) {
try {
session.rpcs[UUID].whep.getStats().then(function (stats) {
if (!(UUID in session.rpcs)) {
return;
}
if (!session.rpcs[UUID].stats["WHEP_Connection"]) {
// meshcast
session.rpcs[UUID].stats["WHEP_Connection"] = {};
}
// var qos = false;]
var nominatedCandidate = false;
var candidates = {};
var ipleakingAllowedRemote = false;
var ipleakingAllowedLocal = false;
stats.forEach(stat => {
if (stat.id && stat.id.startsWith("DEPRECATED_")) {
return;
}
var trackID = stat.trackIdentifier || stat.id || false;
if (stat.type == "remote-candidate") {
candidates[stat.id] = stat;
if (stat.candidateType != "relay") {
ipleakingAllowedRemote = true;
}
} else if (stat.type == "local-candidate") {
candidates[stat.id] = stat;
if (stat.candidateType != "relay") {
ipleakingAllowedLocal = true;
}
} else if (stat.type == "candidate-pair" && stat.nominated) {
if (!nominatedCandidate) {
nominatedCandidate = stat;
} else if (nominatedCandidate.priority < stat.priority) {
nominatedCandidate = stat;
}
} else if (stat.type == "track" && stat.remoteSource) {
if (stat.id in session.rpcs[UUID].stats) {
session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier;
session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0;
session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
if ("frameWidth" in stat) {
if ("frameHeight" in stat) {
session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth + " x " + stat.frameHeight;
session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth;
session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight;
}
}
} else {
session.rpcs[UUID].stats[stat.id] = {};
session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = 0;
session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier;
if (stat.kind && stat.kind == "audio") {
session.rpcs[UUID].stats[stat.id].type = "Audio Track";
session.rpcs[UUID].stats[stat.id]._type = "audio";
} else if (stat.kind && stat.kind == "video") {
session.rpcs[UUID].stats[stat.id].type = "Video Track";
session.rpcs[UUID].stats[stat.id]._type = "video";
}
}
} else if (stat.type == "transport") {
if ("bytesReceived" in stat) {
if ("_bytesReceived" in session.rpcs[UUID].stats["WHEP_Connection"]) {
if (session.rpcs[UUID].stats["WHEP_Connection"]._timestamp) {
if (stat.timestamp) {
session.rpcs[UUID].stats["WHEP_Connection"].total_recv_bitrate_kbps = parseInt((8 * (stat.bytesReceived - session.rpcs[UUID].stats["WHEP_Connection"]._bytesReceived)) / (stat.timestamp - session.rpcs[UUID].stats["WHEP_Connection"]._timestamp));
}
}
}
session.rpcs[UUID].stats["WHEP_Connection"]._bytesReceived = stat.bytesReceived;
}
if ("timestamp" in stat) {
session.rpcs[UUID].stats["WHEP_Connection"]._timestamp = stat.timestamp;
if (!session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart) {
session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart = stat.timestamp;
} else {
session.rpcs[UUID].stats["WHEP_Connection"].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart) / 600) / 100;
}
}
} else if (stat.type == "inbound-rtp" && trackID) {
session.rpcs[UUID].stats[trackID] = session.rpcs[UUID].stats[trackID] || {};
if (stat.trackIdentifier) {
session.rpcs[UUID].stats[trackID]._trackID = stat.trackIdentifier;
}
if ("jitterBufferDelay" in stat) {
session.rpcs[UUID].stats[trackID].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[trackID]._jitter_delay_2)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[trackID]._jitter_count_2)) || 0;
session.rpcs[UUID].stats[trackID]._jitter_delay_2 = parseFloat(stat.jitterBufferDelay) || 0;
session.rpcs[UUID].stats[trackID]._jitter_count_2 = parseInt(stat.jitterBufferEmittedCount) || 0;
}
if ("frameWidth" in stat) {
if ("frameHeight" in stat) {
session.rpcs[UUID].stats[trackID].Resolution = stat.frameWidth + " x " + stat.frameHeight;
session.rpcs[UUID].stats[trackID]._frameWidth = stat.frameWidth;
session.rpcs[UUID].stats[trackID]._frameHeight = stat.frameHeight;
}
}
session.rpcs[UUID].stats[trackID].Bitrate_in_kbps = parseInt((8 * (stat.bytesReceived - (session.rpcs[UUID].stats[trackID]._last_bytes || 0))) / (stat.timestamp - session.rpcs[UUID].stats[trackID]._last_time));
session.rpcs[UUID].stats[trackID]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[trackID]._last_bytes;
session.rpcs[UUID].stats[trackID]._last_time = stat.timestamp || session.rpcs[UUID].stats[trackID]._last_time;
session.rpcs[UUID].stats._codecId = stat.codecId;
session.rpcs[UUID].stats._codecIdTrackId = trackID;
if (stat.mediaType == "video") {
session.rpcs[UUID].stats[trackID].type = "Video Stream";
session.rpcs[UUID].stats[trackID]._type = "video";
if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP8") {
session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0;
session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0;
log("OBS PLI FIX MODE ON");
if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix) {
// heavy packet loss with no pliCount?
session.requestKeyframe(UUID);
session.rpcs[UUID].stats[trackID].nackTrigger = 0;
log("TRYING KEYFRAME");
} else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) {
session.rpcs[UUID].stats[trackID].nackTrigger = 0;
}
} else if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP9") {
session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0;
session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0;
log("OBS PLI FIX MODE ON");
if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix * 4) {
// heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often.
session.requestKeyframe(UUID);
session.rpcs[UUID].stats[trackID].nackTrigger = 0;
log("TRYING KEYFRAME");
} else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) {
session.rpcs[UUID].stats[trackID].nackTrigger = 0;
}
}
session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0;
session.rpcs[UUID].stats[trackID].streamErrors_nackCount = stat.nackCount || 0;
//warnlog(stat);
if ("framesPerSecond" in stat) {
session.rpcs[UUID].stats[trackID].FPS = parseInt(stat.framesPerSecond);
} else if ("framesDecoded" in stat && stat.timestamp) {
var lastFramesDecoded = 0;
var lastTimestamp = 0;
try {
lastFramesDecoded = session.rpcs[UUID].stats[trackID]._framesDecoded;
lastTimestamp = session.rpcs[UUID].stats[trackID]._timestamp;
} catch (e) {}
session.rpcs[UUID].stats[trackID].FPS = parseInt((10 * (stat.framesDecoded - lastFramesDecoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10;
//session.rpcs[UUID].stats[trackID].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp));
session.rpcs[UUID].stats[trackID]._framesDecoded = stat.framesDecoded;
session.rpcs[UUID].stats[trackID]._timestamp = stat.timestamp / 1000;
}
} else if (stat.mediaType == "audio") {
//log("AUDIO LEVEL: "+stat.audioLevel);
session.rpcs[UUID].stats[trackID].type = "Audio Stream";
session.rpcs[UUID].stats[trackID]._type = "audio";
if ("audioLevel" in stat) {
session.rpcs[UUID].stats[trackID].audio_level = parseInt(parseFloat(stat.audioLevel) * 10000) / 10000.0;
}
}
if ("packetsLost" in stat && "packetsReceived" in stat) {
if (!("_packetsLost" in session.rpcs[UUID].stats[trackID])) {
session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost;
}
if (!("_packetsReceived" in session.rpcs[UUID].stats[trackID])) {
session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived;
}
if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[trackID])) {
session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = 0;
}
session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage * 0.35 + (0.65 * ((stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost) * 100.0)) / (stat.packetsReceived - session.rpcs[UUID].stats[trackID]._packetsReceived + (stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost)) || 0;
if (session.rpcs[UUID].stats[trackID]._type === "video") {
qos = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage; // packet loss of video track
}
if (session.rpcs[UUID].signalMeter && session.rpcs[UUID].stats[trackID]._type === "video") {
if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.01) {
if (session.rpcs[UUID].stats[trackID].Bitrate_in_kbps == 0) {
session.rpcs[UUID].signalMeter.dataset.level = 0;
} else {
session.rpcs[UUID].signalMeter.dataset.level = 5;
}
} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.3) {
session.rpcs[UUID].signalMeter.dataset.level = 4;
} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 1.0) {
session.rpcs[UUID].signalMeter.dataset.level = 3;
} else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 3.5) {
session.rpcs[UUID].signalMeter.dataset.level = 2;
} else {
session.rpcs[UUID].signalMeter.dataset.level = 1;
}
}
session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived;
session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost;
}
} else if ("_codecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._codecId) {
if ("mimeType" in stat) {
if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]) {
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
} else {
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {};
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
}
}
if ("frameHeight" in stat) {
if ("frameWidth" in stat) {
session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth) + " x " + parseInt(stat.frameHeight);
}
}
} else if (Firefox) {
if ("mimeType" in stat && "type" in stat && "id" in stat && stat.type == "codec") {
if (stat.mimeType.includes("video")) {
session.rpcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1];
} else if (stat.mimeType.includes("audio")) {
session.rpcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1];
if (stat.clockRate) {
session.rpcs[UUID].stats.audio_clockRate = stat.clockRate;
if (stat.channels) {
session.rpcs[UUID].stats.audio_clockRate += " / " + stat.channels;
}
} else if (stat.sdpFmtpLine) {
session.rpcs[UUID].stats.fmtp = stat.sdpFmtpLine;
}
}
}
if ("frameWidth" in stat) {
session.rpcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight;
if ("framesPerSecond" in stat) {
session.rpcs[UUID].stats.resolution += " @ " + stat.framesPerSecond;
}
}
if ("bytesReceived" in stat) {
if ("kind" in stat && stat.kind == "video") {
if ("_bytesReceived_video" in session.rpcs[UUID].stats) {
session.rpcs[UUID].stats.videoBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_video) / ((1024 * session.statsInterval) / 8000));
}
session.rpcs[UUID].stats._bytesReceived_video = stat.bytesReceived;
} else if ("kind" in stat && stat.kind == "audio") {
if ("_bytesReceived_audio" in session.rpcs[UUID].stats) {
session.rpcs[UUID].stats.audioBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_audio) / ((1024 * session.statsInterval) / 8000));
}
session.rpcs[UUID].stats._bytesReceived_audio = stat.bytesReceived;
}
}
}
});
////////////
if (nominatedCandidate) {
if ("currentRoundTripTime" in nominatedCandidate) {
session.rpcs[UUID].stats["WHEP_Connection"].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime * 1000;
}
}
if (nominatedCandidate && nominatedCandidate.remoteCandidateId) {
if (candidates[nominatedCandidate.remoteCandidateId]) {
var candidate = candidates[nominatedCandidate.remoteCandidateId];
if ("candidateType" in candidate) {
session.rpcs[UUID].stats["WHEP_Connection"].candidateType_remote = candidate.candidateType;
if (candidate.candidateType === "relay") {
if ("relayProtocol" in candidate) {
session.rpcs[UUID].stats["WHEP_Connection"].remote_relay_protocol = candidate.relayProtocol;
}
if ("ip" in candidate) {
session.rpcs[UUID].stats["WHEP_Connection"].remote_relay_IP = candidate.ip;
}
} else {
try {
delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP;
delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol;
} catch (e) {}
}
if ("networkType" in candidate) {
session.rpcs[UUID].stats["WHEP_Connection"].remote_networkType = candidate.networkType;
}
}
}
}
if (nominatedCandidate && nominatedCandidate.localCandidateId) {
if (candidates[nominatedCandidate.localCandidateId]) {
var candidate = candidates[nominatedCandidate.localCandidateId];
if ("candidateType" in candidate) {
session.rpcs[UUID].stats["WHEP_Connection"].candidateType_local = candidate.candidateType;
if (candidate.candidateType === "relay") {
if ("relayProtocol" in candidate) {
session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol = candidate.relayProtocol;
}
if ("ip" in candidate) {
session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP = candidate.ip;
}
} else {
try {
delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP;
delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol;
} catch (e) {}
}
}
if ("networkType" in candidate) {
session.rpcs[UUID].stats["WHEP_Connection"].local_networkType = candidate.networkType;
}
}
}
/* try{ // we want to let meshcast know if our node is getting overloaded, to avoid making it worse
if ((qos!==false) && session.rpcs[UUID].settings && session.rpcs[UUID].settings.url){
var request = new XMLHttpRequest();
var node = session.rpcs[UUID].settings.url.split("https://")[1].split(".meshcast.io")[0];
if (node){
request.open('POST', " https://qos.meshcast.io/?name="+node);
request.send(qos);
}
}
} catch(e){
errorlog(e);
} */
//if (session.buffer!==false){
playoutdelay(UUID); // it will handle itself for now on I guess
//}
});
} catch (e) {
errorlog(e);
}
}
function safeAppendToMenu(menu, text) {
const li = document.createElement("li");
const h2 = document.createElement("h2");
h2.title = text;
h2.textContent = text; // Safely assigns text content, avoiding HTML parsing
li.appendChild(h2);
menu.appendChild(li);
}
function printMyStats(menu, screenshare = false) {
// see: setupStatsMenu
if (!session) {
return;
}
var scrollLeft = getById("menuStatsBox").scrollLeft;
var scrollTop = getById("menuStatsBox").scrollTop;
menu.innerHTML = "";
try {
session.stats.outbound_connections = Object.keys(session.pcs).length;
session.stats.inbound_connections = Object.keys(session.rpcs).length;
} catch (e) {}
try {
var obscam = false;
if (document.querySelector("select#videoSource3")) {
var videoSelect = document.querySelector("select#videoSource3").options;
if (videoSelect.length) {
if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) {
// OBS Virtualcam
obscam = true;
} else if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) {
// OBS Virtualcam
obscam = true;
}
}
}
if (session.streamSrc) {
session.streamSrc.getVideoTracks().forEach(function (track) {
session.currentCameraConstraints = track.getSettings();
if (screen && screen.orientation && screen.orientation.type) {
if (!screen.orientation.type.includes("portrait")) {
if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
}
}
} else if (!window.matchMedia("(orientation: portrait)").matches) {
// legacy
if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) {
session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio;
}
}
if (obscam && parseInt(session.currentCameraConstraints.frameRate) == 30) {
session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0);
} else {
var frameRateFPS = session.currentCameraConstraints.frameRate;
if (frameRateFPS) {
session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0) + " @ " + parseInt(frameRateFPS * 100) / 100.0 + "fps";
} else {
session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0);
}
}
});
}
} catch (e) {
errorlog(e);
}
function printViewValues(obj, UUID = false) {
if (!document.getElementById("menuStatsBox")) {
return;
}
var keys = Object.keys(obj).sort();
keys.forEach(key => {
if (typeof obj[key] === "object") {
try {
let sanitizedKey = sanitizeChat(key);
if (sanitizedKey === "info") {
sanitizedKey = "Remote Peer Info";
}
safeAppendToMenu(menu, sanitizedKey);
} catch (e) {
errorlog(e);
}
try {
printViewValues(obj[key]);
} catch (e) {
errorlog(e);
}
menu.innerHTML += "";
}
});
if (session.promptAccess) {
if (UUID && session.pcs[UUID]) {
menu.innerHTML += "";
}
}
if (UUID && session.pcs[UUID] && session.pcs[UUID].restartIce) {
// only show if available
menu.innerHTML += "";
}
keys.forEach(key => {
if (typeof obj[key] !== "object") {
if (key.startsWith("_")) {
return;
}
let unit = "";
let hint = "";
var stat = sanitizeChat(key);
stat = stat.charAt(0).toUpperCase() + stat.slice(1);
var value = obj[key];
if (typeof value == "string") {
value = sanitizeChat(value);
}
if (value === false) {
return;
}
if (key == "useragent") {
value = "" + value + "";
}
if (key == "local_relay_IP") {
value = "" + value + "";
}
if (key == "remote_relay_IP") {
value = "" + value + "";
}
if (key == "watch_URL") {
value = "" + value + "";
}
if (key == "candidateType_local" && value == "relay") {
stat = "Candidate type - Local";
value = "💸
relay server
";
} else if (key == "candidateType_remote" && value == "relay") {
stat = "Candidate type - Remote";
value = "💸
relay server
";
} else if (key == "candidateType_local" && value == "host") {
stat = "Candidate type - Local";
value = "
host
";
} else if (key == "candidateType_remote" && value == "host") {
stat = "Candidate type - Remote";
value = "
host
";
} else if (key == "candidateType_local" && value == "srflx") {
stat = "Candidate type - Local";
value = "
srflx
";
} else if (key == "candidateType_remote" && value == "srflx") {
stat = "Candidate type - Remote";
value = "
srflx
";
} else if (key == "local_ip_blocking" && value) {
value = "⚠️ You're blocking";
hint = "no direct p2p connection made because of YOUR browser or system setting";
} else if (key == "remote_ip_blocking" && value) {
value = "⚠️ They're blocking";
hint = "no direct p2p connection made because of THEIR browser or system setting";
} else if (key == "label" && value) {
value = "" + value + "";
}
stat = stat.replaceAll("_", " ");
if (hint) {
menu.innerHTML += "
" + stat + "" + value + unit + "
";
} else {
menu.innerHTML += "
" + stat + "" + value + unit + "
";
}
}
});
if (UUID && session.pcs[UUID]) {
if (session.pcs[UUID].maxBandwidth) {
menu.innerHTML += "
max bandwidth target" + session.pcs[UUID].maxBandwidth + "
";
}
if (session.pcs[UUID].setBitrate) {
menu.innerHTML += '
\
";
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";
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) {
if (tmp) {
var lock = target2.querySelector("[data-locked]");
if (lock) {
lock = parseInt(lock.dataset.locked);
}
tmp = tmp.dataset.sid;
slots[tmp] = lock || i + 1;
}
}
}
pokeAPI("positionChange", slots);
}
}
function updateLockedElements() {
var eles = getById("guestFeeds").children;
for (var i = 0; i < eles.length; i++) {
try {
var UUID = eles[i].UUID;
var lock = document.getElementById("position_" + UUID).dataset.locked;
if (parseInt(lock)) {
lockPosition(document.getElementById("position_" + UUID), true);
}
} catch (e) {}
}
}
function lockPosition(ele, apply = false) {
var UUID = ele.dataset.UUID;
if (apply) {
if (ele.dataset.locked && parseInt(ele.dataset.locked)) {
if (getById("guestFeeds")) {
var currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1;
ele.innerHTML = "#" + 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) {
document.getElementById("alertModal").style.left = event.pageX + "px"; // 165px 356px
document.getElementById("alertModal").style.top = event.pageY + 180 + "px";
}
getById("alertModal").addEventListener("click", function (e) {
e.stopPropagation();
return false;
});
} else {
var slot = await promptAlt("Which slot to change to?");
setSlot(ele, slot);
}
}
function setSlot(ele, slot) {
log("setSlot()");
getById("slotPicker").classList.add("hidden");
if (slot !== null) {
try {
slot = parseInt(slot) || 0;
// Find container with stream ID
const container = ele.closest('[data-sid]');
const streamID = container ? container.dataset.sid : null;
if (!streamID) {
return false;
}
// Critical part: Check if the slot is already occupied and swap instead of replace
const existingStreamID = session.currentSlots[slot];
if (existingStreamID && existingStreamID !== streamID) {
// Find the current slot of the stream we're moving
let currentSlot = null;
Object.entries(session.currentSlots).forEach(([key, value]) => {
if (value === streamID) {
currentSlot = parseInt(key);
}
});
// If the stream we're moving is already in a slot, update that slot's value
if (currentSlot !== null) {
// Perform the swap
session.currentSlots[slot] = streamID;
session.currentSlots[currentSlot] = existingStreamID;
// Update the other element's UI
const otherSlotBar = document.querySelector(`[data-sid="${existingStreamID}"][data-slot]`);
if (otherSlotBar) {
otherSlotBar.dataset.slot = currentSlot;
const otherButton = otherSlotBar.querySelector('button');
if (otherButton) {
otherButton.innerText = currentSlot ? `slot: ${currentSlot}` : 'unset';
}
}
// Update UI for the element we're setting
container.dataset.slot = slot;
ele.innerText = slot ? `slot: ${slot}` : 'unset';
// Update pastSlots
session.pastSlots[streamID] = slot;
session.pastSlots[existingStreamID] = currentSlot;
// Update iframes
pokeIframeAPI("slot-updated", slot, null, streamID);
pokeIframeAPI("slot-updated", currentSlot, null, existingStreamID);
} else {
// We're moving a stream that wasn't in a slot before
// First clear any current assignment for this stream
Object.entries(session.currentSlots).forEach(([key, value]) => {
if (value === streamID) {
delete session.currentSlots[key];
}
});
// Then assign it to the new slot
session.currentSlots[slot] = streamID;
// Update UI
container.dataset.slot = slot;
ele.innerText = slot ? `slot: ${slot}` : 'unset';
// Update pastSlots
session.pastSlots[streamID] = slot;
// Update iframe
pokeIframeAPI("slot-updated", slot, null, streamID);
}
} else {
// No conflict, just set the slot normally
// Clear any existing slot for this stream
Object.entries(session.currentSlots).forEach(([key, value]) => {
if (value === streamID) {
delete session.currentSlots[key];
}
});
// Set the new slot
if (slot) {
session.currentSlots[slot] = streamID;
}
// Update UI
container.dataset.slot = slot;
ele.innerText = slot ? `slot: ${slot}` : 'unset';
// Update pastSlots
session.pastSlots[streamID] = slot;
// Update iframe
pokeIframeAPI("slot-updated", slot, null, streamID);
}
// Always update all peers
broadcastSlotUpdate();
} catch (e) {
errorlog(e);
return false;
}
return true;
}
return false;
}
function swapNodes(n1, n2) {
log("swapping nodes");
var p1 = n1.parentNode;
var p2 = n2.parentNode;
var i1, i2;
if (!p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1)) return;
for (var i = 0; i < p1.children.length; i++) {
if (p1.children[i].isEqualNode(n1)) {
i1 = i;
}
}
for (var i = 0; i < p2.children.length; i++) {
if (p2.children[i].isEqualNode(n2)) {
i2 = i;
}
}
if (p1.isEqualNode(p2) && i1 < i2) {
i2++;
}
p1.insertBefore(n2, p1.children[i1]);
p2.insertBefore(n1, p2.children[i2]);
}
function getCurrentSlot(streamID) {
const slotEntry = Object.entries(session.currentSlots).find(([_, sid]) => sid === streamID);
return slotEntry ? slotEntry[0] : false;
}
function getSlotState(UUID) {
if (!UUID || !(UUID in session.rpcs)) return false;
return getCurrentSlot(session.rpcs[UUID].streamID);
}
function combinedLayout(layout) {
if (!Array.isArray(layout)) return layout || {};
var combined = {};
for (var i = 0; i < layout.length; i++) {
if (!layout[i] || !("slot" in layout[i])) {
if (!combined[""]) combined[""] = [];
combined[""].push(layout[i]);
continue;
}
const slotNumber = parseInt(layout[i].slot || 0) + 1;
const streamID = session.currentSlots[slotNumber];
if (!streamID) {
if (!combined[""]) combined[""] = [];
combined[""].push(layout[i]);
continue;
}
combined[streamID] = layout[i];
}
return combined;
}
function syncSlotState(streamID, slotValue=false, updateUI=true) {
// Clear any existing slots for this stream
Object.entries(session.currentSlots).forEach(([slot, sid]) => {
if (sid === streamID) delete session.currentSlots[slot];
});
// Set new slot if one provided
if (slotValue) {
session.currentSlots[slotValue] = streamID;
}
// Update UI if requested
if (updateUI) {
const slotsBar = document.querySelector(`[data-sid="${streamID}"][data-slot]`);
if (slotsBar) {
slotsBar.dataset.slot = slotValue;
const slotButton = slotsBar.querySelector('button');
if (slotButton) {
slotButton.innerText = slotValue ? `slot: ${slotValue}` : 'unset';
}
}
}
pokeIframeAPI("slot-updated", slotValue, null, streamID); // need to support self-director
session.pastSlots[streamID] = slotValue || 0;
clearTimeout(session.slotBroadcastThrottle);
session.slotBroadcastThrottle = setTimeout(function(){broadcastSlotUpdate();},10);
return true;
}
function broadcastSlotUpdate(UUID = false) {
try {
if (!session.slotmode || !session.director){
return;
}
if (!UUID) {
if (session.slotBroadcastThrottle){
clearTimeout(session.slotBroadcastThrottle);
session.slotBroadcastThrottle = null;
}
session.sendMessage({ slotsUpdate: session.currentSlots });
} else {
session.sendMessage({ slotsUpdate: session.currentSlots }, UUID);
}
} catch (e) {
errorlog(e);
}
}
function updateSlotUI() {
// Update all slot UI elements based on the current state in session.currentSlots
Object.entries(session.currentSlots).forEach(([slot, streamID]) => {
const slotBar = document.querySelector(`[data-sid="${streamID}"][data-slot]`);
if (slotBar) {
slotBar.dataset.slot = slot;
const button = slotBar.querySelector('button');
if (button) {
button.innerText = slot ? `slot: ${slot}` : 'unset';
}
}
});
}
function createControlBox(UUID, soloLink, streamID, slot_init = false) {
if (document.getElementById("deleteme")) {
getById("deleteme").parentNode.removeChild(getById("deleteme"));
}
var controls = getById("controls_blank").cloneNode(true);
controls.classList.remove("hidden");
controls.id = "controls_" + UUID;
var container = document.createElement("div");
container.className = "vidcon directorMargins";
container.id = "container_" + UUID; // needed to delete on user disconnect
container.UUID = UUID;
container.dataset.UUID = UUID;
container.dataset.sid = streamID;
if (session.orderby) {
try {
var added = false;
for (var i = 0; i < getById("guestFeeds").children.length; i++) {
if (getById("guestFeeds").children[i].UUID && !getById("guestFeeds").children[i].shifted) {
if (getById("guestFeeds").children[i].UUID in session.rpcs) {
if (session.rpcs[getById("guestFeeds").children[i].UUID].streamID.toLowerCase() > streamID.toLowerCase()) {
getById("guestFeeds").insertBefore(container, getById("guestFeeds").children[i]);
added = true;
break;
}
}
}
}
if (!added) {
getById("guestFeeds").appendChild(container);
}
} catch (e) {
getById("guestFeeds").appendChild(container);
}
} else {
getById("guestFeeds").appendChild(container);
}
//controls.innerHTML += "";
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) {
var biggestSlot = 0;
var slotDefault = null;
// Handle initial slot value from initialization or past slots
if (slot_init && session.slotmode == 1) {
slotDefault = slot_init || null;
}
if (streamID in session.pastSlots) {
slotDefault = session.pastSlots[streamID];
}
// Get all current slots from RPC state
var allSlots = [];
if (session.slotmode == 1) {
Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => {
if (currentSlot) {
if (parseInt(currentSlot) > biggestSlot) {
biggestSlot = parseInt(currentSlot);
}
if (slotDefault === parseInt(currentSlot)) {
slotDefault = null;
}
allSlots.push(parseInt(currentSlot));
}
});
biggestSlot += 1;
} else if (slotDefault !== null && session.slotmode == 2) {
// Check if slot is already in use by any remote participant
const remoteSlotInUse = Object.keys(session.rpcs).some(UUID =>
getSlotState(UUID) === slotDefault
);
// Check if slot is used by the director
const directorSlotInUse = Object.entries(session.currentSlots).some(([slot, sid]) =>
parseInt(slot) === slotDefault && (sid === session.streamID || sid === session.streamID+":s")
);
if (remoteSlotInUse || directorSlotInUse) {
slotDefault = null; // This slot is already in use
}
}
// Determine final slot value
if (slotDefault !== null) {
// the default slot is available
biggestSlot = slotDefault;
} else if (slot_init && session.slotmode == 1) {
// was manually set, so can't be something else but 0
biggestSlot = 0;
} else if (session.slotmode == 1) {
var bestfree = 0;
for (var i = 1; i <= biggestSlot; i++) {
if (allSlots.includes(i)) {
continue;
} else {
bestfree = i;
break;
}
}
biggestSlot = bestfree;
}
// Set slot name
var slotName = biggestSlot ? "slot: " + biggestSlot : "unset";
buttons +=
`
`;
// 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);
}
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";
session.pastSlots[streamID] = biggestSlot;
// Build HTML with same structure
buttons +=
`
`;
// Sync the initial state
syncSlotState(streamID, biggestSlot, false); // false since UI is being created here
}
buttons +=
"
ID: " +
streamID +
"\
\
\
\
";
container.innerHTML = buttons;
updateLockedElements();
var videoContainerControlBox = document.createElement("div");
videoContainerControlBox.className = "controlVideoBox";
container.containerControlBox = videoContainerControlBox;
container.appendChild(videoContainerControlBox);
videoContainerControlBox.appendChild(videoContainer);
if (session.signalMeter) {
if (!session.rpcs[UUID].signalMeter) {
session.rpcs[UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true);
session.rpcs[UUID].signalMeter.id = "signalMeter_" + UUID;
session.rpcs[UUID].signalMeter.dataset.level = 0;
session.rpcs[UUID].signalMeter.classList.remove("hidden");
session.rpcs[UUID].signalMeter.dataset.UUID = UUID;
session.rpcs[UUID].signalMeter.title = getTranslation("signal-meter");
//if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.cpu_maxed){
// session.rpcs[UUID].signalMeter.dataset.cpu = "1";
//}
session.rpcs[UUID].signalMeter.addEventListener("click", function (e) {
// show stats of video if double clicked
log("clicked signal meter");
try {
e.preventDefault();
if (session.statsMenu !== false) {
var uid = e.currentTarget.dataset.UUID;
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
}
e.stopPropagation();
return false;
} catch (e) {
errorlog(e);
}
});
}
videoContainer.appendChild(session.rpcs[UUID].signalMeter);
}
if (session.batteryMeter) {
////////
if (!session.rpcs[UUID].batteryMeter) {
session.rpcs[UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true);
session.rpcs[UUID].batteryMeter.id = "batteryMeter_" + UUID;
batteryMeterInfoUpdate(UUID);
}
videoContainer.appendChild(session.rpcs[UUID].batteryMeter);
}
if (session.showConnections) {
if (!session.rpcs[UUID].connectionDetails) {
createConnectionDetailsEle(UUID);
}
videoContainer.appendChild(session.rpcs[UUID].connectionDetails);
}
videoContainer.appendChild(session.rpcs[UUID].voiceMeter);
videoContainer.appendChild(session.rpcs[UUID].remoteMuteElement);
videoContainer.appendChild(session.rpcs[UUID].remoteVideoMuteElement);
videoContainer.appendChild(session.rpcs[UUID].remoteRaisedHandElement);
videoContainer.appendChild(iframeDetails);
videoContainer.appendChild(session.rpcs[UUID].videoElement);
container.appendChild(controls);
session.group.forEach(group => {
var ele = controls.querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]');
if (!ele) {
var newGroup = htmlToElement('");
var added = false;
container.querySelectorAll(".customGroup>[data-group]").forEach(ele => {
log(ele);
if (!added && ele.dataset.group > group + "") {
ele.parentNode.insertBefore(newGroup, ele);
added = true;
}
});
if (!added) {
var newGroupCon = container.querySelector(".customGroup");
if (!newGroupCon) {
newGroupCon = document.createElement("div");
newGroupCon.classList.add("customGroup");
container.appendChild(newGroupCon);
}
newGroupCon.appendChild(newGroup);
}
}
});
initSceneList(UUID);
pokeIframeAPI("control-box", true, UUID);
}
function remoteRemoveQueue(ele) {
let ts = { ...transferSettings };
ts.justResetting = true;
session.directMigrateIssue(session.roomid, ts, ele.dataset.UUID);
ele.classList.add("hidden");
}
function minimizeMe(button, director = false) {
if (!director) {
getById("container_" + button.dataset.UUID).classList.toggle("minimized");
} else {
getById(director).classList.toggle("minimized");
}
}
function blackoutMode() {
var overlay = document.getElementById("blackoutOverlay");
if (!overlay) {
overlay = document.createElement('div');
overlay.id = "blackoutOverlay";
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
color: white;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
cursor: pointer;
z-index: 9999;
`;
overlay.textContent = 'Click to exit black-out mode';
document.body.appendChild(overlay);
} else {
overlay.classList.remove("hidden");
}
function exitBlackout() {
overlay.classList.add("hidden");
overlay.removeEventListener('click', exitBlackout);
}
overlay.addEventListener('click', exitBlackout);
}
function cycleCameras() {
if (session.screenShareState) {
warnUser("Stop the screen-share first.");
return;
}
var videoSelect = document.querySelector("select#videoSource3").options;
// don't show flip option if only one camera.
// don't show if not a mobile device
// don't show if AD=0
var matched = false;
var maxIndex = parseInt(getById("flipcamerabutton").dataset.maxIndex) || parseInt(videoSelect.length);
if (maxIndex > parseInt(videoSelect.length)) {
maxIndex = parseInt(videoSelect.length);
}
for (var i = 0; i < maxIndex; i++) {
var selOption = videoSelect[i];
if (selOption.selected) {
matched = true;
} else if (matched) {
if (getById("flipcamerabutton").classList.contains("flip")) {
getById("flipcamerabutton").classList.remove("flip");
getById("flipcamerabutton").classList.add("flip2");
} else {
getById("flipcamerabutton").classList.remove("flip2");
getById("flipcamerabutton").classList.add("flip");
}
document.querySelector("select#videoSource3").value = selOption.value;
activatedPreview = false;
grabVideo(session.quality, "videosource", "select#videoSource3");
return;
}
}
for (var i = 0; i < maxIndex; i++) {
var selOption = videoSelect[i];
if (selOption.selected) {
return; // do nothing; the camera that is selected is the only camera available it seems.
} else {
if (getById("flipcamerabutton").classList.contains("flip")) {
getById("flipcamerabutton").classList.remove("flip");
getById("flipcamerabutton").classList.add("flip2");
} else {
getById("flipcamerabutton").classList.remove("flip2");
getById("flipcamerabutton").classList.add("flip");
}
document.querySelector("select#videoSource3").value = selOption.value;
activatedPreview = false;
grabVideo(session.quality, "videosource", "select#videoSource3");
return;
}
}
}
function addToGoogleCalendar() {
var title = "Live Stream";
//var dates = "20180512T230000Z/20180513T030000Z";
var linkout = getById("director_block_1").innerText;
var details = "Join the live stream as a performer at the following link:
===> " + 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();
}
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") {
if (session.audioDevice.includes(deviceInfos[i].deviceId)) {
matched1.push(deviceInfos[i]);
} else if (session.audioDevice.includes(normalizeDeviceLabel(deviceInfos[i].label))) {
matched1.push(deviceInfos[i]);
} else {
for (var j = 0; j < session.audioDevice.length; j++) {
if (normalizeDeviceLabel(deviceInfos[i].label).includes(session.audioDevice[j])) {
matched2.push(deviceInfos[i]);
log("A DEVICE FOUND = " + deviceInfos[i].label);
break;
}
}
}
} else {
notmatched.push(deviceInfos[i]);
}
}
matched2.sort((a, b) => {
if (a.label && b.label) {
if (a.label.length < b.label.length){
return -1
}
return a.label.localeCompare(b.label, undefined, {sensitivity: 'base'});
}
return 0;
});
var matched = matched1.concat(matched2);
deviceInfos = matched.concat(notmatched);
} else if (session.store && session.store.SelectedAudioInputDevices) {
var matched = [];
var notmatch = [];
for (let i = 0; i < deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (session.store.SelectedAudioInputDevices.includes(deviceInfo.deviceId)) {
matched.push(deviceInfo);
log("EXACT A DEVICE FOUND -- from saved session");
} else {
notmatch.push(deviceInfo);
}
}
deviceInfos = matched.concat(notmatch);
}
if (session.sink || SelectedAudioOutputDevices) {
// this sorts according to users's manual selection
var matched = [];
var notmatch = [];
for (let i = 0; i !== deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (deviceInfo.kind === "audiooutput" && deviceInfo.deviceId === session.sink) {
matched.push(deviceInfo);
} else if (!session.sink && deviceInfo.kind === "audiooutput" && deviceInfo.deviceId === SelectedAudioOutputDevices) {
matched.push(deviceInfo);
} else {
notmatch.push(deviceInfo);
}
}
deviceInfos = matched.concat(notmatch);
}
if (session.videoDevice && session.videoDevice !== 1) {
var tmp = [];
var tmp2 = [];
var tmp3 = [];
var deviceIdMatch = false;
// First pass - check for label matches
for (let i = 0; i !== deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).startsWith(session.videoDevice)) {
tmp.push(deviceInfo);
log("Starts With V DEVICE FOUND");
} else if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice)) {
tmp2.push(deviceInfo);
log("Includes With V DEVICE FOUND");
} else {
tmp3.push(deviceInfo);
}
}
// If no label matches found, try device ID match
if (tmp.length === 0 && tmp2.length === 0) {
for (let i = 0; i < tmp3.length; ++i) {
deviceInfo = tmp3[i];
if (deviceInfo.kind === "videoinput" && deviceInfo.deviceId === session.videoDevice) {
tmp.push(deviceInfo);
deviceIdMatch = true;
log("EXACT DEVICE ID MATCH FOUND");
break;
}
}
}
if (tmp2.length && !deviceIdMatch) {
tmp = tmp.concat(tmp2);
}
if (tmp3.length) {
tmp = tmp.concat(tmp3);
}
deviceInfos = tmp;
log("VDEVICE:" + session.videoDevice);
log(deviceInfos);
} else if (session.videoDevice === false && session.facingMode) {
var tmp = [];
if (session.facingMode == "environment") {
for (let i = 0; i !== deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("back")) {
tmp.push(deviceInfo);
log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
} else if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("rear")) {
tmp.push(deviceInfo);
log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
}
}
} else if (session.facingMode == "user") {
for (let i = 0; i !== deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("front")) {
tmp.push(deviceInfo);
log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
}
}
}
for (let i = 0; i !== deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (!(deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice))) {
if (deviceInfo.deviceId !== session.videoDevice) {
tmp.push(deviceInfo);
}
}
}
deviceInfos = tmp;
log("VDECICE:" + session.videoDevice);
log(deviceInfos);
} else if (session.store && session.store.SelectedVideoInputDevices && session.videoDevice === false) {
var matched = [];
var notmatch = [];
for (let i = 0; i !== deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (session.store.SelectedVideoInputDevices.includes(deviceInfo.deviceId)) {
matched.push(deviceInfo);
log("EXACT V DEVICE FOUND -- from saved session");
} else {
notmatch.push(deviceInfo);
}
}
deviceInfos = matched.concat(notmatch);
delete session.store.SelectedVideoInputDevices;
} else if (session.mobile){
var tmp = [];
for (let i = 0; i !== deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("front")) {
tmp.push(deviceInfo);
log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label));
}
}
for (let i = 0; i !== deviceInfos.length; ++i) {
deviceInfo = deviceInfos[i];
if (!(deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice))) {
if (deviceInfo.deviceId !== session.videoDevice) {
tmp.push(deviceInfo);
}
}
}
deviceInfos = tmp;
log("AUTO FRONT:" + session.videoDevice);
log(deviceInfos);
}
if (session.audioDevice && typeof session.audioDevice == "object") {
var adMatch = [...session.audioDevice];
} else if (session.store && session.store.SelectedAudioInputDevices && session.store.SelectedAudioInputDevices.length) {
var adMatch = [...session.store.SelectedAudioInputDevices];
} else {
var adMatch = false;
}
if (session.store && session.store.SelectedAudioInputDevices) {
delete session.store.SelectedAudioInputDevices;
}
var counter = 1;
for (let i = 0; i !== deviceInfos.length; ++i) {
var deviceInfo = deviceInfos[i];
if (deviceInfo == null) {
continue;
}
if (deviceInfo.kind === "audioinput") {
option = document.createElement("input");
option.type = "checkbox";
counter++;
listele = document.createElement("li");
listele.style.display = "none";
if (typeof adMatch == "object") {
for (var j = 0; j < adMatch.length; j++) {
if (!adMatch[j]) {
// skip, already matched
} else if (adMatch[j] == deviceInfo.deviceId) {
option.checked = true;
listele.style.display = "block";
option.style.display = "none";
getById("multiselect1").checked = false;
try {
getById("multiselect1").parentNode.style.display = "none";
} catch (e) {}
adMatch[j] = null;
break;
} else if (normalizeDeviceLabel(deviceInfo.label).includes(adMatch[j])) {
option.checked = true;
listele.style.display = "block";
option.style.display = "none";
getById("multiselect1").checked = false;
try {
getById("multiselect1").parentNode.style.display = "none";
} catch (e) {}
adMatch[j] = null;
break;
}
}
}
if (typeof adMatch !== "object" && counter == 2) {
option.checked = true;
listele.style.display = "block";
option.style.display = "none";
getById("multiselect1").checked = false;
try {
getById("multiselect1").parentNode.style.display = "none";
} catch (e) {}
}
option.value = deviceInfo.deviceId || "default";
option.name = "multiselect" + counter;
option.id = "multiselect" + counter;
option.label = deviceInfo.label;
label = document.createElement("label");
label.for = option.name;
label.innerHTML = " " + (deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1));
listele.appendChild(option);
listele.appendChild(label);
audioInputSelect.appendChild(listele);
option.onchange = function (event) {
// make sure to clear 'no audio option' if anything else is selected
getById("multiselect1").checked = false;
log("UNCHECKED");
if (!CtrlPressed) {
SelectedAudioInputDevices = [];
audioInputSelect.querySelectorAll("input[type='checkbox']").forEach(function (item) {
if (event.currentTarget.id !== item.id) {
item.checked = false;
} else {
item.checked = true;
SelectedAudioInputDevices = [event.currentTarget.value];
}
});
} else {
if (event.currentTarget.checked) {
if (!SelectedAudioInputDevices) {
SelectedAudioInputDevices = [event.currentTarget.value];
} else if (!SelectedAudioInputDevices.includes(event.currentTarget.value)) {
SelectedAudioInputDevices.push(event.currentTarget.value);
}
} else if (event.currentTarget.value) {
while (SelectedAudioInputDevices.includes(event.currentTarget.value)) {
SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(event.currentTarget.value), 1);
}
}
}
if (session.mobile && !(iOS || iPad) && event.currentTarget.label === "USB audio" && !session.cleanOutput) {
warnUser("Notice: USB audio devices may not work on all mobile devices.\n\nConsider using FireFox mobile instead, as it tends to work with USB audio devices more often.");
}
saveSettings();
};
if (deviceInfo.label.includes("Yeti ")) {
if (!session.cleanOutput) {
//getById("audioTipContext1").innerHTML = getTranslation("blue-yeti-tip");
miniTranslate(getById("audioTipContext1"), "blue-yeti-tip");
getById("audioTip1").classList.remove("hidden");
}
}
} else if (deviceInfo.kind === "videoinput") {
option = document.createElement("option");
option.value = deviceInfo.deviceId || "default";
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
} else if (deviceInfo.kind === "audiooutput") {
option = document.createElement("option");
if (audioOutputSelect.length === 0) {
option.dataset.default = true;
} else {
option.dataset.default = false;
}
option.value = deviceInfo.deviceId || "default";
if (option.value == session.sink) {
option.selected = "true";
} else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices == option.value) {
option.selected = "true";
}
option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`;
audioOutputSelect.appendChild(option);
} else {
log("Some other kind of source/device: ", deviceInfo);
}
}
if (Firefox && !session.mobile) {
var option = document.createElement("option");
option.value = "others";
option.text = getTranslation("show-more-options");
audioOutputSelect.appendChild(option);
}
if (audioOutputSelect.childNodes.length == 0) {
option = document.createElement("option");
option.value = "default";
option.text = getTranslation("system-default");
audioOutputSelect.appendChild(option);
}
option = document.createElement("option");
option.text = getTranslation("disable-video");
option.value = "ZZZ";
videoSelect.appendChild(option); // NO AUDIO OPTION
if (miconly) {
option.selected = "true";
}
selectors.forEach((select, selectorIndex) => {
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
select.value = values[selectorIndex];
}
});
} catch (e) {
errorlog(e);
}
}
function getUserMediaVideoParams(resolutionFallbackLevel, isSafariBrowser) {
switch (resolutionFallbackLevel) {
case -1:
return {};
case -2:
if (isSafariBrowser) {
return {
width: {
min: 360,
ideal: 3840,
max: 3840
},
height: {
min: 360,
ideal: 2160,
max: 2160
}
};
} else if (Firefox) {
return {
width: {
ideal: 3840
},
height: {
ideal: 2160
}
};
} else {
return {
width: {
min: 720,
ideal: 3840,
max: 3840
},
height: {
min: 720,
ideal: 2160,
max: 2160
}
};
}
case -3:
if (isSafariBrowser) {
return {
width: {
min: 360,
ideal: 2560,
max: 1440
},
height: {
min: 360,
ideal: 1440,
max: 1440
}
};
} else if (Firefox) {
return {
width: {
ideal: 2560
},
height: {
ideal: 1440
}
};
} else {
return {
width: {
min: 720,
ideal: 2560,
max: 2560
},
height: {
min: 720,
ideal: 1440,
max: 1440
}
};
}
case 0:
if (isSafariBrowser) {
return {
width: {
min: 360,
ideal: 1920,
max: 1920
},
height: {
min: 360,
ideal: 1080,
max: 1080
}
};
} else if (Firefox) {
return {
width: {
ideal: 1920
},
height: {
ideal: 1080
}
};
} else {
return {
width: {
min: 720,
ideal: 1920,
max: 1920
},
height: {
min: 720,
ideal: 1080,
max: 1920
}
};
}
case 1:
if (isSafariBrowser) {
return {
width: {
min: 360,
ideal: 1280,
max: 1280
},
height: {
min: 360,
ideal: 720,
max: 720
}
};
} else if (Firefox) {
return {
width: {
ideal: 1280
},
height: {
ideal: 720
}
};
} else {
return {
width: {
min: 720,
ideal: 1280,
max: 1280
},
height: {
min: 720,
ideal: 720,
max: 1280
}
};
}
case 2:
if (isSafariBrowser) {
return {
width: {
min: 640
},
height: {
min: 360
}
};
} else if (Firefox) {
return {
width: {
ideal: 640
},
height: {
ideal: 360
}
};
} else {
return {
width: {
min: 240,
ideal: 640,
max: 1280
},
height: {
min: 240,
ideal: 360,
max: 1280
}
};
}
case 3:
if (isSafariBrowser) {
return {
width: {
min: 360,
ideal: 1280,
max: 1440
}
};
} else {
return {
width: {
min: 360,
ideal: 1280,
max: 1440
}
};
}
case 4:
if (isSafariBrowser) {
return {
height: {
min: 360,
ideal: 720,
max: 960
}
};
} else {
return {
height: {
ideal: 720,
max: 960
}
};
}
case 5:
if (isSafariBrowser) {
return {
width: {
min: 360,
ideal: 640,
max: 1440
},
height: {
min: 360,
ideal: 360,
max: 720
}
};
} else {
return {
width: {
ideal: 640,
max: 1920
},
height: {
ideal: 360,
max: 1920
}
}; // same as default, but I didn't want to mess with frameRates until I gave it all a try first
}
case 6:
if (isSafariBrowser) {
return {}; // iphone users probably don't need to wait any longer, so let them just get to it
} else {
return {
width: {
min: 360,
ideal: 640,
max: 3840
},
height: {
min: 360,
ideal: 360,
max: 2160
}
};
}
case 7:
return {
// If the camera is recording in low-light, it may have a low frameRate. It coudl also be recording at a very high resolution.
width: {
min: 360,
ideal: 640
},
height: {
min: 360,
ideal: 360
}
};
case 8:
return {
width: {
min: 360
},
height: {
min: 360
},
frameRate: 10
}; // same as default, but I didn't want to mess with frameRates until I gave it all a try first
case 9:
return {
frameRate: 0
}; // Some Samsung Devices report they can only support a frameRate of 0.
case 10:
return {};
default:
return {};
}
}
function addScreenDevices(device) {
if (device.kind == "audio") {
const audioInputSelect = getById("audioSource3");
const listele = document.createElement("li");
listele.style.display = "block";
const option = document.createElement("input");
option.type = "checkbox";
option.checked = true;
if (getById("multiselect-trigger3").dataset.state == 0) {
option.style.display = "none";
}
option.value = device.id;
option.name = device.label;
option.dataset.type = "screen";
option.label = device.label;
const label = document.createElement("label");
label.for = option.name;
label.innerHTML = " " + device.label;
listele.appendChild(option);
listele.appendChild(label);
option.onchange = function (event) {
// make sure to clear 'no audio option' if anything else is selected
log("change 4644");
if (!CtrlPressed) {
document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
if (!item.value) {
return;
}
if (event.currentTarget.value !== item.value) {
// this shoulnd't happen, but if it does.
item.checked = false;
if (item.dataset.type == "screen") {
item.parentElement.parentElement.removeChild(item.parentElement);
}
while (SelectedAudioInputDevices.indexOf(item.value) > -1) {
SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1);
}
activatedPreview = false;
grabAudio("#audioSource3"); // exclude item.id
} else {
if (SelectedAudioInputDevices.indexOf(item.value) == -1) {
if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
SelectedAudioInputDevices = [];
}
SelectedAudioInputDevices.push(item.value);
}
item.checked = true;
activatedPreview = false;
grabAudio("#audioSource3", item.value); // exclude item.id. we will reconnect, even if already connected, as a way to 'reset' a device if it isn't working.
}
});
}
saveSettings();
event.stopPropagation();
return false;
};
audioInputSelect.appendChild(listele);
getById("audioSourceNoAudio2").checked = false;
} else if (device.kind == "video") {
const videoSelect = getById("videoSource3");
//const selectors = [ videoSelect];
//const values = selectors.map(select => select.value);
const option = document.createElement("option");
option.value = device.id;
option.text = device.label;
option.selected = "true";
option.label = device.label;
videoSelect.appendChild(option);
}
}
var gotDevices2AlreadyRan = false;
function gotDevices2(deviceInfos) {
gotDevices2AlreadyRan = true;
log("got devices!2");
log(deviceInfos);
getById("multiselect-trigger3").dataset.state = "0";
getById("multiselect-trigger3").classList.add("closed");
getById("multiselect-trigger3").classList.remove("open");
getById("chevarrow2").classList.add("bottom");
if (!session.streamSrc) {
checkBasicStreamsExist();
}
var knownTrack = false;
try {
const audioInputSelect = getById("audioSource3");
const videoSelect = getById("videoSource3");
const audioOutputSelect = getById("outputSource3");
const selectors = [videoSelect];
[audioInputSelect].forEach(select => {
while (select.firstChild) {
select.removeChild(select.firstChild);
}
});
const values = selectors.map(select => select.value);
selectors.forEach(select => {
while (select.firstChild) {
select.removeChild(select.firstChild);
}
});
[audioOutputSelect].forEach(select => {
while (select.firstChild) {
select.removeChild(select.firstChild);
}
});
var counter = 0;
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
if (deviceInfo == null) {
continue;
}
if (deviceInfo.kind === "audioinput") {
var option = document.createElement("input");
option.type = "checkbox";
counter++;
var listele = document.createElement("li");
listele.style.display = "none";
session.streamSrc.getAudioTracks().forEach(function (track) {
if (deviceInfo.label == track.label) {
option.checked = true;
listele.style.display = "inherit";
}
});
option.style.display = "none";
option.value = deviceInfo.deviceId || "default";
option.name = "multiselecta" + counter;
option.id = "multiselecta" + counter;
option.dataset.label = deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1);
var label = document.createElement("label");
label.for = option.name;
label.innerHTML = " " + (deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1));
listele.appendChild(option);
listele.appendChild(label);
audioInputSelect.appendChild(listele);
option.onchange = function (event) {
// make sure to clear 'no audio option' if anything else is selected
log("change 4768");
if (!CtrlPressed) {
document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
if (event.currentTarget.value !== item.value) {
item.checked = false;
if (item.dataset.type == "screen") {
item.parentElement.parentElement.removeChild(item.parentElement);
}
while (SelectedAudioInputDevices.indexOf(item.value) > -1) {
SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1);
}
} else {
item.checked = true;
if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) {
if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
SelectedAudioInputDevices = [];
}
SelectedAudioInputDevices.push(event.currentTarget.value);
}
}
});
} else {
if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) {
if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
SelectedAudioInputDevices = [];
}
SelectedAudioInputDevices.push(event.currentTarget.value);
}
getById("audioSourceNoAudio2").checked = false;
}
saveSettings();
};
} else if (deviceInfo.kind === "videoinput") {
var option = document.createElement("option");
option.value = deviceInfo.deviceId || "default";
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
try {
if (!knownTrack && session.canvasSource) {
session.canvasSource.srcObject.getVideoTracks().forEach(function (track) {
if (option.text == track.label) {
option.selected = "true";
knownTrack = true;
}
});
}
if (!knownTrack && session.streamSrc) {
session.streamSrc.getVideoTracks().forEach(function (track) {
if (option.text == track.label) {
option.selected = "true";
knownTrack = true;
}
});
}
} catch (e) {
errorlog(e);
}
videoSelect.appendChild(option);
} else if (deviceInfo.kind === "audiooutput") {
var option = document.createElement("option");
if (audioOutputSelect.length === 0) {
option.dataset.default = true;
} else {
option.dataset.default = false;
}
option.value = deviceInfo.deviceId || "default";
if (option.value == session.sink) {
option.selected = "true";
} else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices == option.value) {
option.selected = "true";
session.sink = option.value; // added 8-dec-22, as the director's saved mic wasn't applying otherwise.
}
option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`;
audioOutputSelect.appendChild(option);
} else {
log("Some other kind of source/device: ", deviceInfo);
}
}
if (Firefox && !session.mobile) {
var option = document.createElement("option");
option.value = "others";
option.text = getTranslation("show-more-options");
audioOutputSelect.appendChild(option);
}
if (audioOutputSelect.childNodes.length == 0) {
var option = document.createElement("option");
option.value = "default";
option.text = getTranslation("system-default");
audioOutputSelect.appendChild(option);
}
if (videoSelect.childNodes.length <= 1) {
getById("flipcamerabutton").style.display = "none"; // don't show the camera cycle button
getById("flipcamerabutton").dataset.maxndex = videoSelect.childNodes.length;
} else {
getById("flipcamerabutton").style.display = "unset";
getById("flipcamerabutton").dataset.maxIndex = videoSelect.childNodes.length;
}
////////////
session.streamSrc.getAudioTracks().forEach(function (track) {
// add active ScreenShare audio tracks to the list
log("Checking for screenshare audio");
var matched = false;
for (var i = 0; i !== deviceInfos.length; ++i) {
var deviceInfo = deviceInfos[i];
if (deviceInfo == null) {
continue;
}
log("---");
if (track.label == deviceInfo.label) {
matched = true;
continue;
}
}
if (matched == false) {
// Not a gUM device
var listele = document.createElement("li");
listele.style.display = "block";
var option = document.createElement("input");
option.type = "checkbox";
option.value = track.id;
option.checked = true;
option.style.display = "none";
option.name = track.label;
option.label = track.label;
option.dataset.type = "screen";
var label = document.createElement("label");
label.for = option.name;
label.innerHTML = " " + track.label;
listele.appendChild(option);
listele.appendChild(label);
option.onchange = function (event) {
// make sure to clear 'no audio option' if anything else is selected
log("change 4873");
var trackid = null;
if (!CtrlPressed) {
document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
if (event.currentTarget.value !== item.value) {
// this shoulnd't happen, but if it does.
item.checked = false;
if (item.dataset.type == "screen") {
item.parentElement.parentElement.removeChild(item.parentElement);
}
} else {
event.currentTarget.checked = true;
trackid = item.value;
}
});
} else {
//getById("audioSourceNoAudio2").checked=false;
if (event.currentTarget.dataset.type == "screen") {
event.currentTarget.parentElement.parentElement.removeChild(event.currentTarget.parentElement);
}
}
activatedPreview = false;
grabAudio("#audioSource3", trackid); // exclude item.id.
event.stopPropagation();
return false;
};
audioInputSelect.appendChild(listele);
}
});
/////////// no video option
var optionss = false;
if (screensharesupport) {
optionss = document.createElement("option");
optionss.text = "Screen Share (replace camera)";
optionss.value = "XXX";
videoSelect.appendChild(optionss); // NO AUDIO OPTION
}
var option = document.createElement("option"); // no video
option.text = getTranslation("disable-video");
option.value = "ZZZ";
videoSelect.appendChild(option);
if (session.streamSrc.getVideoTracks().length == 0) {
option.selected = "true";
} else if (knownTrack == false) {
var option = document.createElement("option"); // no video
option.text = session.streamSrc.getVideoTracks()[0].label;
option.value = "YYY";
videoSelect.appendChild(option);
option.selected = "true";
}
if (optionss) {
optionss.lastSelected = videoSelect.selectedIndex;
}
videoSelect.onchange = function (event) {
try {
if (event.target.options[event.target.options.selectedIndex].value === "XXX") {
videoSelect.selectedIndex = event.target.options[event.target.options.selectedIndex].lastSelected;
if (session.screenShareState == false) {
toggleScreenShare();
} else {
toggleScreenShare(true);
}
return;
}
} catch (e) {}
activatedPreview = false;
grabVideo(session.quality, "videosource", "select#videoSource3");
if (!getById("audioSource3").querySelectorAll("input[data-type='screen']").length) {
if (session.screenShareState) {
session.screenShareState = false;
pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
notifyOfScreenShare();
//session.refreshScale();
}
getById("screensharebutton").classList.remove("green");
getById("screensharebutton").ariaPressed = "false";
}
};
///////////// /// NO AUDIO appended option
var option = document.createElement("input");
option.type = "checkbox";
option.value = "ZZZ";
option.style.display = "none";
option.id = "audioSourceNoAudio2";
var label = document.createElement("label");
label.for = option.name;
label.innerHTML = " No Audio";
var listele = document.createElement("li");
if (session.streamSrc.getAudioTracks().length == 0) {
option.checked = true;
} else {
listele.style.display = "none";
option.checked = false;
}
option.onchange = function (event) {
// make sure to clear 'no audio option' if anything else is selected
log("change 4938");
if (!CtrlPressed) {
document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
if (event.currentTarget.value !== item.value) {
item.checked = false;
if (item.dataset.type == "screen") {
item.parentElement.parentElement.removeChild(item.parentElement);
}
while (SelectedAudioInputDevices.indexOf(item.value) > -1) {
SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1);
}
} else {
item.checked = true;
if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) {
if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
SelectedAudioInputDevices = [];
}
SelectedAudioInputDevices.push(event.currentTarget.value);
}
}
});
} else {
document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) {
if (event.currentTarget.value === item.value) {
event.currentTarget.checked = true;
if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) {
if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
SelectedAudioInputDevices = [];
}
SelectedAudioInputDevices.push(event.currentTarget.value);
}
} else {
item.checked = false;
if (item.dataset.type == "screen") {
item.parentElement.parentElement.removeChild(item.parentElement);
}
while (SelectedAudioInputDevices.indexOf(item.value) > -1) {
SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1);
}
}
});
}
saveSettings();
};
listele.appendChild(option);
listele.appendChild(label);
audioInputSelect.appendChild(listele);
////////////
//selectors.forEach((select, selectorIndex) => {
// if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
// select.value = values[selectorIndex];
// }
//});
audioInputSelect.onchange = function () {
log("Audio OPTION HAS CHANGED? 2");
activatedPreview = false;
setTimeout(function () {
grabAudio("#audioSource3");
}, 10);
};
getById("refreshVideoButton").onclick = function () {
refreshVideoDevice();
};
if (Firefox && !session.mobile && navigator.mediaDevices) {
audioOutputSelect.onclick = function () {
log("audioOutputSelect.onclick = function() {");
if (audioOutputSelect.options[audioOutputSelect.selectedIndex].value === "others") {
log("Trying to increase the output device list");
navigator.mediaDevices.selectAudioOutput().then(device => {
if (device.kind == "audiooutput") {
session.sink = device.deviceId;
try {
var matched = false;
audioOutputSelect.childNodes.forEach(ele => {
if (ele.value === device.deviceId) {
matched = true;
ele.selected = true;
}
});
if (!matched) {
var option = document.createElement("option");
option.value = device.deviceId;
option.text = device.label;
audioOutputSelect.appendChild(option);
option.selected = true;
}
saveSettings(); // we're saving because there was an explicit action to change devices
} catch (e) {
errorlog(e);
}
if (!session.sink) {
return;
} // Not sure this would ever happen, but whatever.
resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist.
}
});
}
};
} else if (!navigator.mediaDevices){
console.warn("No navigator.mediaDevices found - try a different browser or check your settings.");
}
audioOutputSelect.onchange = function () {
log("audioOutputSelect.onchange = function() {");
if (iOS || iPad) {
return;
}
if (Firefox && !session.mobile) {
if (audioOutputSelect.options[audioOutputSelect.selectedIndex].value === "others") {
// we handle this elsewhere
return;
}
}
try {
session.sink = audioOutputSelect.options[audioOutputSelect.selectedIndex].value;
saveSettings();
} catch (e) {
errorlog(e);
}
if (!session.sink) {
return;
}
resetupAudioOut();
log("done audioOutputSelect.onchange = function() {");
};
} catch (e) {
errorlog(e);
}
}
function refreshVideoDevice() {
if (session.screenShareState) {
log("can't refresh a screenshare");
return;
}
log("video source changed");
activatedPreview = false;
grabVideo(session.quality, "videosource", "select#videoSource3");
}
function refreshMicrophoneDevice(UUID=false) {
if (session.screenShareState || session.mediafileShare) {
log("can't refresh a screenshare or fileshare");
if (UUID){
var data = {};
data.UUID = UUID;
data.rejected = "can't refresh mic during screen or file share";
session.sendMessage(data, data.UUID);
}
return;
}
log("refreshing microphone..");
activatedPreview = false;
grabAudio("#audioSource3", null, false, UUID);
}
function directRefreshMicrophone(ele){
var UUID = ele.dataset.UUID;
if (!UUID){return;}
// remoteAudioLabel_
// '[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]'
if (getById("remoteAudioLabel_" + UUID).dataset.nomic){
warnUser("No microphone is enabled for this user\n\nNothing to reresh.");
}
var data = {};
data.refreshMicrophone = true;
data.UUID = UUID;
if (session.sendRequest(data, UUID)){ // Viewer is requesting the PUBLISHER
ele.classList.add("pressed");
setTimeout((ele)=>{
if(ele){
ele.classList.remove("pressed");
}
},400,ele);
}
}
function gotDevicesRemote(deviceInfos, UUID) {
try {
if (document.getElementById("remoteVideoSelect_" + UUID)) {
var videoSelect = document.getElementById("remoteVideoSelect_" + UUID);
var length = videoSelect.options.length;
for (i = length - 1; i >= 0; i--) {
videoSelect.options[i] = null;
}
} else {
var videoSelect = document.createElement("select");
videoSelect.id = "remoteVideoSelect_" + UUID;
videoSelect.onchange = function () {
if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) {
getById("requestVideoDevice_" + UUID).innerHTML = ' apply';
getById("requestVideoDevice_" + UUID).title = "This will update the remote device to the selected one";
} else {
getById("requestVideoDevice_" + UUID).innerHTML = ' request';
getById("requestVideoDevice_" + UUID).title = "This will ask the remote guest for permission to change";
}
};
var buttonGO = document.createElement("button");
buttonGO.innerHTML = ' refresh';
buttonGO.title = "This will refresh the current device";
buttonGO.id = "requestVideoDevice_" + UUID;
buttonGO.onclick = function () {
var data = {};
data.changeCamera = videoSelect.value;
data.UUID = UUID;
session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER
};
var videoSelectDiv = document.createElement("div");
query("#container_" + UUID + " .advancedVideoSettings").appendChild(videoSelectDiv);
videoSelectDiv.appendChild(videoSelect);
videoSelectDiv.appendChild(buttonGO);
}
if (document.getElementById("remoteAudioSelect_" + UUID)) {
log("remoteAudioSelect_ ");
var audioSelect = document.getElementById("remoteAudioSelect_" + UUID);
var length = audioSelect.options.length;
for (i = length - 1; i >= 0; i--) {
audioSelect.options[i] = null;
}
} else {
var audioSelect = document.createElement("select");
audioSelect.id = "remoteAudioSelect_" + UUID;
audioSelect.onchange = function () {
log("ON CHANGE");
if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) {
getById("requestAudioDevice_" + UUID).innerHTML = ' apply';
getById("requestAudioDevice_" + UUID).title = "This will update the remote device to the selected one";
} else {
getById("requestAudioDevice_" + UUID).innerHTML = ' request';
getById("requestAudioDevice_" + UUID).title = "This will ask the remote guest for permission to change";
}
};
var buttonGO = document.createElement("button");
buttonGO.innerHTML = ' refresh';
// buttonGO.style = "padding: 5px;";
buttonGO.title = "This will refresh the current device";
buttonGO.id = "requestAudioDevice_" + UUID;
buttonGO.onclick = function () {
var data = {};
data.changeMicrophone = audioSelect.value;
data.UUID = UUID;
session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER
};
var audioSelectDiv = document.createElement("div");
query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioSelectDiv);
audioSelectDiv.appendChild(audioSelect);
audioSelectDiv.appendChild(buttonGO);
}
if (document.getElementById("remoteAudioOutputSelect_" + UUID)) {
var audioOutputSelect = document.getElementById("remoteAudioOutputSelect_" + UUID);
var length = audioOutputSelect.options.length;
for (i = length - 1; i >= 0; i--) {
audioOutputSelect.options[i] = null;
}
} else {
var audioOutputSelect = document.createElement("select");
audioOutputSelect.id = "remoteAudioOutputSelect_" + UUID;
audioOutputSelect.onchange = function () {
if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) {
getById("requestAudioOutputDevice_" + UUID).innerHTML = ' apply';
getById("requestAudioOutputDevice_" + UUID).title = "This will update the remote device to the selected one";
} else {
getById("requestAudioOutputDevice_" + UUID).innerHTML = ' request';
getById("requestAudioOutputDevice_" + UUID).title = "This will ask the remote guest for permission to change";
}
};
var buttonGO = document.createElement("button");
buttonGO.innerHTML = ' refresh';
buttonGO.title = "This will refresh the current device";
buttonGO.id = "requestAudioOutputDevice_" + UUID;
buttonGO.onclick = function () {
var data = {};
data.changeSpeaker = audioOutputSelect.value;
data.UUID = UUID;
session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER
};
var audioOutputSelectContainer = document.createElement("div");
query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioOutputSelectContainer);
audioOutputSelectContainer.appendChild(audioOutputSelect);
audioOutputSelectContainer.appendChild(buttonGO);
}
var matched = false;
var audiomatched = false;
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
if (deviceInfo == null) {
continue;
}
if (deviceInfo.kind === "videoinput") {
const option = document.createElement("option");
option.value = deviceInfo.deviceId || "default";
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
if (getById("remoteVideoLabel_" + UUID).innerText == option.text) {
option.selected = "true";
matched = true;
}
videoSelect.appendChild(option);
} else if (deviceInfo.kind === "audioinput") {
const option = document.createElement("option");
option.value = deviceInfo.deviceId || "default";
option.text = deviceInfo.label || `microphone ${audioSelect.length + 1}`;
if (getById("remoteAudioLabel_" + UUID).innerText == option.text) {
option.selected = "true";
audiomatched = true;
}
audioSelect.appendChild(option);
} else if (deviceInfo.kind === "audiooutput") {
const option = document.createElement("option");
option.value = deviceInfo.deviceId || "default";
option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;
if (getById("remoteAudioOutputSelect_" + UUID).innerText == option.text) {
option.selected = "true";
}
audioOutputSelect.appendChild(option);
}
}
if (!matched) {
getById("requestVideoDevice_" + UUID).innerHTML = ' request';
getById("requestVideoDevice_" + UUID).title = "This will ask the remote guest for permission to change";
}
if (!audiomatched) {
getById("requestAudioDevice_" + UUID).innerHTML = ' request';
getById("requestAudioDevice_" + UUID).title = "This will ask the remote guest for permission to change";
}
} catch (e) {
errorlog(e);
}
pokeIframeAPI("remote-devices-info", deviceInfos, UUID);
}
var timeoutTone = false;
function playtone(screen = false, tonename = "testtone") {
if (timeoutTone) return;
if (session.beepToNotify && tonename){
if (session.beepToNotify!==true){
let val = tonename.split("tone")[0];
if (!session.beepToNotify.includes(val)){
return;
}
}
}
const type = tonename.replace("tone", "");
if (session.notifyTypes && !session.notifyTypes.includes(type)) return;
timeoutTone = true;
setTimeout(() => timeoutTone = false, 500);
const toneEle = document.getElementById(tonename);
if (!toneEle) return;
if (iOS || iPad) {
toneEle.muted = false;
toneEle.play().catch(e => console.error('Playback failed:', e));
return;
}
if (screen && getById("outputSourceScreenshare")) {
session.sink = getById("outputSourceScreenshare").value;
saveSettings();
}
if (session.sink) {
toneEle.setSinkId(session.sink)
.then(() => toneEle.play())
.catch(() => toneEle.play());
} else {
toneEle.play();
}
}
function updateAudioSource(newUrl, name="testtone") {
var audioElement = document.getElementById(name);
if (!audioElement){
audioElement = createAudioElement();
audioElement.id = name;
getById("testtone").parentNode.insertBefore(audioElement, getById("testtone").nextSibling);
}
audioElement.src = newUrl;
var sources = audioElement.getElementsByTagName("source");
var extension = newUrl.split(".").pop().toLowerCase();
var mimeType;
switch (extension) {
case "mp3":
mimeType = "audio/mpeg";
break;
case "wav":
mimeType = "audio/wav";
break;
case "ogg":
mimeType = "audio/ogg";
break;
case "aac":
case "m4a":
mimeType = "audio/aac";
break;
case "opus":
mimeType = "audio/opus";
break;
case "flac":
mimeType = "audio/flac";
break;
case "webm":
mimeType = "audio/webm";
break;
default:
console.error("Unsupported file type:", extension);
return;
}
if (sources && sources.length === 1) {
sources[0].src = newUrl;
sources[0].type = mimeType;
} else {
audioElement.innerHTML = "";
var newSource = document.createElement("source");
newSource.src = newUrl;
newSource.type = mimeType;
audioElement.appendChild(newSource);
}
audioElement.load();
}
function showNotification(title, body = "") {
if (!Notification) {
return;
}
if (Notification.permission !== "granted") {
Notification.requestPermission();
} else {
let icon = "/media/old_icon.png"; //this is a large image may take more time to show notifiction, replace with small size icon
let notification = new Notification(title, { body, icon });
notification.onclick = () => {
notification.close();
window.parent.focus();
};
}
}
function toFirefoxConstraint(chromeConstraint) {
if (!chromeConstraint || typeof chromeConstraint !== 'object') {
return chromeConstraint;
}
const result = { ...chromeConstraint };
if (result.audio && typeof result.audio === 'object') {
result.audio = flattenConstraints(result.audio);
}
if (result.video && typeof result.video === 'object') {
result.video = flattenConstraints(result.video);
}
return result;
}
function flattenConstraints(constraints) {
const result = { ...constraints };
for (const [key, value] of Object.entries(constraints)) {
if (value && typeof value === 'object') {
if ('exact' in value) {
result[key] = value.exact;
} else if ('ideal' in value) {
result[key] = value.ideal;
}
}
}
return result;
}
async function getAudioOnly(selector, trackid = null, override = false) {
var audioSelect = document.querySelector(selector).querySelectorAll("input,option");
var audioList = [];
var streams = [];
log("getAudioOnly()");
for (var i = 0; i < audioSelect.length; i++) {
if (audioSelect[i].value == "ZZZ") {
continue;
} else if (trackid == audioSelect[i].value) {
// skip already excluded
continue;
} else if ("screen" == audioSelect[i].dataset.type) {
// skip already excluded ---------- !!!!!! DOES THIS MAKE SENSE? TODO: CHECK
continue;
} else if (audioSelect[i].checked) {
log(audioSelect[i]);
audioList.push(audioSelect[i]);
} else if (audioSelect[i].selected) {
log(audioSelect[i]);
audioList.push(audioSelect[i]);
}
}
for (var i = 0; i < audioList.length; i++) {
if (session.echoCancellation !== false && session.autoGainControl !== false && session.noiseSuppression !== false && (session.voiceIsolation !== true)) {
var constraint = {
audio: {
deviceId: {
exact: audioList[i].value
}
}
};
} else {
// Just trying to avoid problems with some systems that don't support these features
var constraint = {
audio: {
deviceId: {
exact: audioList[i].value
}
}
};
if (session.echoCancellation === false) {
constraint.audio.echoCancellation = false;
} else {
constraint.audio.echoCancellation = true;
}
if (session.autoGainControl === false) {
constraint.audio.autoGainControl = false;
} else {
constraint.audio.autoGainControl = true;
}
if (session.noiseSuppression === false) {
constraint.audio.noiseSuppression = false;
} else {
constraint.audio.noiseSuppression = true;
}
if (session.voiceIsolation === true){
constraint.audio.voiceIsolation = true;
}
}
constraint.video = false;
if (override !== false) {
log("Override true");
if (override.audio && override.audio.deviceId) {
if (audioList[i].value == override.audio.deviceId) {
constraint = override;
} else {
// not the device we want to hack.
}
} else {
constraint = override;
}
}
if (audioList[i].value && SelectedAudioInputDevices) {
if (SelectedAudioInputDevices.indexOf(audioList[i].value) === -1) {
if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) {
SelectedAudioInputDevices = [];
}
SelectedAudioInputDevices.push(audioList[i].value);
}
}
if (session.audioInputChannels) {
if (constraint.audio === true) {
constraint.audio = {};
constraint.audio.channelCount = session.audioInputChannels;
} else if (constraint.audio) {
constraint.audio.channelCount = session.audioInputChannels;
}
}
if (session.micSampleRate) {
if (constraint.audio === true) {
constraint.audio = {};
constraint.audio.sampleRate = parseInt(session.micSampleRate);
} else if (constraint.audio) {
constraint.audio.sampleRate = parseInt(session.micSampleRate);
}
}
if (session.micSampleSize) {
if (constraint.audio === true) {
constraint.audio = {};
constraint.audio.sampleSize = parseInt(session.micSampleSize);
} else if (constraint.audio) {
constraint.audio.sampleSize = parseInt(session.micSampleSize);
}
}
log("CONSTRAINT");
log(constraint);
if (Firefox){
constraint = toFirefoxConstraint(constraint);
}
warnlog("navigator.mediaDevices.getUserMedia starting...");
if (navigator.mediaDevices){
var stream = await navigator.mediaDevices
.getUserMedia(constraint)
.then(function (stream2) {
log("get audio sucecss");
pokeIframeAPI("local-microphone-event");
return stream2;
})
.catch(function (err) {
warnlog(err);
if (!session.cleanOutput) {
if (override !== false) {
if (err.name) {
if (err.constraint) {
warnUser(err["name"] + ": " + err["constraint"]);
}
}
}
}
return false;
}); // More error reporting maybe?
if (stream) {
streams.push(stream);
}
} else {
console.warn("navigator.mediaDevices was not found; try a different browser or check your settings");
}
}
return streams;
}
function applyMirror(mirror) {
// true unmirrors as its already mirrored
if (!session.videoElement) {
return;
}
try {
var transFlip = "";
var transNorm = "";
if (document.getElementById("videosource") && session.windowed) {
transFlip = " translate(0, 50%)";
transNorm = " translate(0, -50%)";
}
if (session.mirrored == 2) {
mirror = true;
} else if (session.mirrored === 0) {
mirror = true;
}
if (!session.videoElement.style) {
session.videoElement.style = "";
}
if (session.permaMirrored) {
mirror = !mirror;
}
if (mirror) {
if (session.mirrored && session.flipped) {
session.videoElement.style.transform = "scaleX(-1) scaleY(-1)" + transFlip;
session.videoElement.classList.add("mirrorControl");
session.videoElement.dataset.transform = "scaleX(-1) scaleY(-1)";
} else if (session.mirrored) {
session.videoElement.style.transform = "scaleX(-1)" + transNorm;
session.videoElement.classList.add("mirrorControl");
session.videoElement.dataset.transform = "scaleX(-1)";
} else if (session.flipped) {
session.videoElement.style.transform = "scaleY(-1) scaleX(1)" + transFlip;
session.videoElement.classList.remove("mirrorControl");
session.videoElement.dataset.transform = "scaleX(1) scaleY(-11)";
} else {
session.videoElement.style.transform = "scaleX(1)" + transNorm;
session.videoElement.classList.remove("mirrorControl");
session.videoElement.dataset.transform = "scaleX(1)";
}
} else {
if (session.mirrored && session.flipped) {
session.videoElement.style.transform = "scaleX(1) scaleY(-1)" + transFlip;
session.videoElement.classList.remove("mirrorControl");
session.videoElement.dataset.transform = "scaleX(1) scaleY(-1)";
} else if (session.mirrored) {
session.videoElement.style.transform = "scaleX(1)" + transNorm;
session.videoElement.classList.remove("mirrorControl");
session.videoElement.dataset.transform = "scaleX(1)";
} else if (session.flipped) {
session.videoElement.style.transform = "scaleY(-1) scaleX(-1)" + transFlip;
session.videoElement.classList.add("mirrorControl");
session.videoElement.dataset.transform = "scaleX(-1) scaleY(-1)";
} else {
session.videoElement.style.transform = "scaleX(-1)" + transNorm;
session.videoElement.classList.add("mirrorControl");
session.videoElement.dataset.transform = "scaleX(-1)";
}
}
var rotate = 0;
if (session.forceRotate !== false) {
if (session.rotate) {
rotate = session.forceRotate * -1 + parseInt(session.rotate);
} else {
rotate = session.forceRotate * -1;
}
if (session.forceRotate) {
rotate += 180;
}
} else {
rotate = session.rotate;
}
if (rotate && rotate >= 360) {
rotate -= 360;
}
session.videoElement.rotated = rotate;
session.videoElement.dataset.rotated = rotate;
if (document.getElementById("previewWebcam") || document.getElementById("videosource")) {
var eleName = document.getElementById("previewWebcam") || document.getElementById("videosource");
if (rotate) {
if (eleName.style.transform) {
eleName.style.transform += " rotate(" + rotate + "deg)";
} else {
eleName.style.transform = "rotate(" + rotate + "deg)";
}
eleName.classList.add("rotate");
} else {
eleName.classList.remove("rotate");
}
} else if (document.getElementById("container")) {
if (rotate == 0) {
document.getElementById("container").classList.remove("rotate");
document.getElementById("container").style.transform = "unset";
document.getElementById("container").style.transformOrigin = "unset";
} else {
document.getElementById("container").style.transform = "rotate(" + rotate + "deg)";
}
} else if (document.getElementById("minipreview")) {
var eleName = document.getElementById("minipreview");
if (rotate == 90) {
eleName.style.transform = "rotate(90deg)";
eleName.style.transformOrigin = "50% 100%";
eleName.style.height = eleName.style.width;
eleName.style.width = "unset";
} else if (session.videoElement.rotated == 270) {
eleName.style.transform = "rotate(270deg)";
eleName.style.transformOrigin = "50% 100%";
eleName.style.width = "unset";
eleName.style.height = eleName.style.width;
} else if (session.videoElement.rotated == 180) {
eleName.style.transform = "rotate(180deg)";
eleName.style.transformOrigin = "unset";
} else {
eleName.classList.remove("rotate");
eleName.style.transform = "unset";
eleName.style.transformOrigin = "unset";
}
}
// if not one of these, then it's going to be handled by the automixer automatically for us.
} catch (e) {
errorlog(e);
}
}
function applyMirrorGuest(mirror, videoElement) {
// true unmirrors as its already mirrored
try {
if (mirror) {
videoElement.style.transform = "scaleX(-1)";
videoElement.classList.add("mirrorControl");
} else {
videoElement.style.transform = "scaleX(1)";
videoElement.classList.remove("mirrorControl");
}
} catch (e) {
errorlog(e);
}
}
function cleanupMediaTracks() {
getUserMediaRequestID += 1;
try {
if (session.streamSrcClone) {
session.streamSrcClone.getTracks().forEach(function (track) {
session.streamSrcClone.removeTrack(track);
track.stop();
log("stopping old track");
});
}
if (session.streamSrc) {
session.streamSrc.getTracks().forEach(function (track) {
session.streamSrc.removeTrack(track);
track.stop();
log("stopping old track");
});
} else {
checkBasicStreamsExist();
}
if (session.videoElement && session.videoElement.srcObject) {
session.videoElement.srcObject.getTracks().forEach(function (track) {
session.videoElement.srcObject.removeTrack(track);
track.stop();
log("stopping old track");
});
} else {
session.videoElement.srcObject = createMediaStream();
}
activatedPreview = false;
} catch (e) {
errorlog(e);
}
getById("gowebcam").dataset.ready = "false";
getById("gowebcam").dataset.audioready = "false";
getById("gowebcam").disabled = true;
}
/// Detect system changes; handle change or use for debugging
var lastAudioDevice = null;
var lastVideoDevice = null;
var lastPlaybackDevice = null;
var audioReconnectTimeout = null;
var videoReconnectTimeout = null;
var grabDevicesTimeout = null;
var playbackReconnectTimeout = null;
function reconnectDevices(event) {
/// TODO: Perhaps change this to only if there is a DISCONNECT; rather than ON NEW DEVICE?
try {
if (session.firstPlayTriggered && session.audioCtx.state == "suspended") {
session.audioCtx.resume();
}
} catch (e) {
warnlog("session.audioCtx.resume(); failed");
}
warnlog("A media device has changed");
if (iOS || iPad) {
// consider adding this back, but if no problem, whatever.
return;
}
if (document.getElementById("previewWebcam")) {
// rest of the code isn't setup to support pre-connection setup.
clearTimeout(playbackReconnectTimeout);
playbackReconnectTimeout = setTimeout(function () {
if (document.getElementById("previewWebcam")) {
enumerateDevices()
.then(gotDevices)
.then(function () {
if (document.getElementById("previewWebcam")) {
resetupAudioOut(document.getElementById("previewWebcam"));
}
});
}
}, 1000);
return;
}
try {
if (!session.streamSrc) {
checkBasicStreamsExist();
} else {
session.streamSrc.getTracks().forEach(function (track) {
if (track.readyState == "ended") {
if (track.kind == "audio") {
lastAudioDevice = track.label;
} else if (track.kind == "video") {
lastVideoDevice = track.label;
}
session.streamSrc.removeTrack(track);
log("remove ended old track");
}
});
if (session.streamSrcClone) {
session.streamSrcClone.getTracks().forEach(function (track) {
if (track.readyState == "ended") {
session.streamSrcClone.removeTrack(track);
track.stop();
}
});
}
if (session.videoElement && session.videoElement.srcObject) {
session.videoElement.srcObject.getTracks().forEach(function (track) {
if (track.readyState == "ended") {
session.videoElement.srcObject.removeTrack(track);
log("remove ended old track");
}
});
}
}
/* } else {
clearTimeout(playbackReconnectTimeout);
playbackReconnectTimeout = setTimeout(function() {
enumerateDevices().then(gotDevices2).then(function() {
resetupAudioOut();
});
}, 1000);
return;
} */
} catch (e) {
errorlog(e);
}
clearTimeout(audioReconnectTimeout);
audioReconnectTimeout = null;
if (lastAudioDevice) {
audioReconnectTimeout = setTimeout(function () {
// only reconnect same audio device. If reconnected, clear the disconnected flag.
enumerateDevices()
.then(gotDevices2)
.then(function () {
// TODO: check to see if any audio is connected?
var streamConnected = false;
var audioSelect = getById("audioSource3").querySelectorAll("input");
for (var i = 0; i < audioSelect.length; i++) {
if (audioSelect[i].value == "ZZZ") {
continue;
} else if (audioSelect[i].checked) {
log("checked");
streamConnected = true;
break;
}
}
if (!streamConnected) {
for (var i = 0; i < audioSelect.length; i++) {
if (audioSelect[i].value == "ZZZ") {
continue;
}
if (lastAudioDevice == audioSelect[i].dataset.label) {
// if the last disconnected device matches.
audioSelect[i].checked = true;
streamConnected = true;
lastAudioDevice = null;
warnlog("DISCONNECTED AUDIO DEVICE RECONNECTED");
break;
}
}
}
activatedPreview = false;
grabAudio("#audioSource3");
setTimeout(function () {
enumerateDevices()
.then(gotDevices2)
.then(function () {});
}, 1000);
});
}, 2000);
}
clearTimeout(videoReconnectTimeout); // only reconnect same video device.
videoReconnectTimeout = null;
if (lastVideoDevice) {
videoReconnectTimeout = setTimeout(function () {
enumerateDevices()
.then(gotDevices2)
.then(function () {
var streamConnected = false;
var videoSelect = getById("videoSource3");
errorlog(videoSelect.value);
if (videoSelect.value == "ZZZ") {
for (var i = 0; i < videoSelect.options.length; i++) {
try {
if (videoSelect.options[i].innerHTML == lastVideoDevice) {
videoSelect.options[i].selected = "true";
streamConnected = true;
lastVideoDevice = null;
break;
}
} catch (e) {
errorlog(e);
}
}
}
if (streamConnected) {
activatedPreview = false;
grabVideo(session.quality, "videosource", "select#videoSource3");
setTimeout(function () {
enumerateDevices()
.then(gotDevices2)
.then(function () {});
}, 1000);
}
});
}, 2000);
}
clearTimeout(playbackReconnectTimeout);
playbackReconnectTimeout = setTimeout(function () {
enumerateDevices()
.then(gotDevices2)
.then(function () {
resetupAudioOut();
});
}, 1000);
}
function handleAudioTrackEnded(event) {
errorlog("Audio track ended unexpectedly");
// If there's already a reconnection attempt in progress, don't start another one
if (session.audioReconnectInProgress) {
return;
}
let modalID = null;
if (!session.cleanOutput) {
warnUser("Your microphone disconnected. Attempting to reconnect...", 3200);
}
session.audioReconnectInProgress = true;
// Wait a brief moment to ensure the device has time to be recognized again if it was unplugged/replugged
setTimeout(function() {
activatedPreview = false;
grabAudio("#audioSource3", null, false);
// Check if reconnection was successful after a delay
setTimeout(function() {
session.audioReconnectInProgress = false;
closeModal(false, modalID);
// Check if there are any active audio tracks after reconnection attempt
const hasAudioTracks = session.streamSrc &&
session.streamSrc.getAudioTracks &&
session.streamSrc.getAudioTracks().length > 0;
if (!hasAudioTracks) {
// Reconnection failed, open settings menu
if (!session.cleanOutput) {
warnUser("Failed to reconnect your microphone. Please select a different device.", 5000);
}
// Open the settings menu
if (typeof toggleSettings === 'function') {
toggleSettings(true); // force show the settings
}
}
}, 2000);
}, 1000);
}
var vingesterFixed = false;
function resetupAudioOut(ele = false, forceReset = false) {
// this re-sets ALL output devices / sources
log("resetupAudioOut");
if (iOS || iPad || SafariVersion || (ChromiumVersion && session.mobile)) {
// TODO : TEST TO SEE IF THIS WORKS WITH SAFARI? it might.
if (ele) {
return;
}
for (var UUID in session.rpcs) {
if (session.rpcs[UUID].videoElement) {
try {
session.rpcs[UUID].videoElement
.pause()
.then(() => {
setTimeout(
function (uuid) {
log("win");
try {
session.rpcs[uuid].videoElement.play().then(() => {
// updateIncomingAudioElement(uuid); // DO NOT DO THIS; it would cause a loop, as this updateIncomingAudioElement calls reseet
log("toggle pause/play");
});
} catch (e) {
errorlog(e);
}
},
0,
UUID
);
})
.catch(errorlog);
} catch (e) {
warnlog(e);
}
}
}
return;
}
//if (isVingester){return;}
var outputSelect = document.getElementById("outputSource") || document.getElementById("outputSource3") || false;
var sinkSet = false;
try {
// function tries to get vingester working, by listening for its first tweak.
if (!session.sink && !vingesterFixed && document.getElementById("testtone") && document.getElementById("testtone").sinkId && isVingester) {
vingesterFixed = true; // only set the session.sink one time, as vingester on should be needing to set it one time.
session.sink = document.getElementById("testtone").sinkId;
}
} catch (e) {
errorlog(e);
}
if (outputSelect && outputSelect.options && outputSelect.options.length) {
for (var i = 0; i < outputSelect.options.length; i++) {
if (outputSelect.options[i].value == session.sink) {
outputSelect.options[i].selected = "true";
sinkSet = true;
}
}
if (sinkSet == false) {
if (outputSelect.options[0]) {
outputSelect.options[0].selected = "true";
sinkSet = outputSelect.value;
}
} else {
sinkSet = session.sink;
}
} else {
sinkSet = session.sink;
}
if (ele) {
try {
var eleSink = sinkSet;
if (ele.manualSink) {
eleSink = ele.manualSink;
log("Manual Sink Identified");
}
if (eleSink) {
if (forceReset) {
ele.setSinkId("default")
.then(() => {
ele.setSinkId(eleSink)
.then(() => {
log("New Output Device");
})
.catch(error => {
if (!Firefox) {
warnlog(error);
}
// TODO: If error, then see if I need to add mic support, and grab it if needed.
});
})
.catch(error => {
if (!Firefox) {
errorlog(error);
}
ele.setSinkId(eleSink)
.then(() => {
log("New Output Device");
})
.catch(error => {
if (!Firefox) {
warnlog(error);
}
// TODO: If error, then see if I need to add mic support, and grab it if needed.
});
});
}
ele.setSinkId(eleSink)
.then(() => {
log("New Output Device for self-preview");
})
.catch(error => {
if (!Firefox) {
warnlog("Need to add mic support, and grab it if needed.");
warnlog(error);
}
// TODO: If error, then see if I need to add mic support, and grab it if needed.
});
}
} catch (e) {
warnlog("can't use setsink");
}
log("audio sink: " + eleSink);
return;
}
if (session.videoElement) {
// this would be a preview or videosource
try {
var eleSink = sinkSet;
if (session.videoElement.manualSink) {
eleSink = session.videoElement.manualSink;
}
if (eleSink) {
session.videoElement
.setSinkId(eleSink)
.then(() => {
log("New Output Device for self-preview");
})
.catch(error => {
if (!Firefox) {
warnlog(error);
}
// TODO: If error, then see if I need to add mic support, and grab it if needed.
});
}
} catch (e) {
warnlog("can't use setsink");
}
}
for (UUID in session.rpcs) {
try {
if (session.rpcs[UUID].videoElement) {
var eleSink = sinkSet;
if (session.rpcs[UUID].videoElement.manualSink) {
eleSink = session.rpcs[UUID].videoElement.manualSink;
}
if (eleSink) {
session.rpcs[UUID].videoElement
.setSinkId(eleSink)
.then(() => {
log("New Output Device for: " + UUID);
})
.catch(error => {
if (!Firefox) {
warnlog(error);
}
// TODO: If error, then see if I need to add mic support, and grab it if needed.
});
}
}
} catch (e) {
warnlog(e);
}
}
log("audio sink 2: " + eleSink);
}
function obfuscateURL(input) {
if (input.startsWith("https://obs.ninja/")) {
input = input.replace("https://obs.ninja/", "obs.ninja/");
} else if (input.startsWith("http://obs.ninja/")) {
input = input.replace("http://obs.ninja/", "obs.ninja/");
} else if (input.startsWith("obs.ninja/")) {
input = input.replace("obs.ninja/", "obs.ninja/");
} else if (input.startsWith("https://vdo.ninja/")) {
input = input.replace("https://vdo.ninja/", "vdo.ninja/");
} else if (input.startsWith("http://vdo.ninja/")) {
input = input.replace("http://vdo.ninja/", "vdo.ninja/");
} else if (input.startsWith("vdo.ninja/")) {
input = input.replace("vdo.ninja/", "vdo.ninja/");
}
input = input.replace("&view=", "&v=");
input = input.replace("&view&", "&v&");
input = input.replace("?view&", "?v&");
input = input.replace("?view=", "?v=");
input = input.replace("&videobitrate=", "&vb=");
input = input.replace("?videobitrate=", "?vb=");
input = input.replace("&bitrate=", "&vb=");
input = input.replace("?bitrate=", "?vb=");
input = input.replace("?audiodevice=", "?ad=");
input = input.replace("&audiodevice=", "&ad=");
input = input.replace("?label=", "?l=");
input = input.replace("&label=", "&l=");
input = input.replace("?stereo=", "?s=");
input = input.replace("&stereo=", "&s=");
input = input.replace("&stereo&", "&s&");
input = input.replace("?stereo&", "?s&");
input = input.replace("?webcam&", "?wc&");
input = input.replace("&webcam&", "&wc&");
input = input.replace("?remote=", "?rm=");
input = input.replace("&remote=", "&rm=");
input = input.replace("?password=", "?p=");
input = input.replace("&password=", "&p=");
input = input.replace("&maxvideobitrate=", "&mvb=");
input = input.replace("?maxvideobitrate=", "?mvb=");
input = input.replace("&maxbitrate=", "&mvb=");
input = input.replace("?maxbitrate=", "?mvb=");
input = input.replace("&height=", "&h=");
input = input.replace("?height=", "?h=");
input = input.replace("&width=", "&w=");
input = input.replace("?width=", "?w=");
input = input.replace("&quality=", "&q=");
input = input.replace("?quality=", "?q=");
input = input.replace("&cleanoutput=", "&clean=");
input = input.replace("?cleanoutput=", "?clean=");
input = input.replace("&maxviewers=", "&clean=");
input = input.replace("?maxviewers=", "?clean=");
input = input.replace("&frameRate=", "&fr=");
input = input.replace("?frameRate=", "?fr=");
input = input.replace("&fps=", "&fr=");
input = input.replace("?fps=", "?fr=");
input = input.replace("&roomid=", "&r=");
input = input.replace("?roomid=", "?r=");
input = input.replace("&room=", "&r=");
input = input.replace("?room=", "?r=");
log(input);
var key = "OBSNINJAFORLIFE";
var encrypted = CryptoJS.AES.encrypt(input, key);
var output = "https://invite.cam/" + encrypted.toString();
return output;
}
function notifyOfScreenShare() {
if (session.notifyScreenShare) {
var data = {};
data.screenShareState = session.screenShareState;
session.sendMessage(data);
}
}
var beforeScreenShare = null; // video
var screenShareAudioTrack = null;
async function toggleScreenShare(reload = false) {
/// &sstype=1
var quality = session.quality_ss;
if (quality === false) {
quality = session.roomid ? session.quality_room : session.quality_wb;
}
if (session.quality !== false) {
quality = session.quality;
}
if (session.screensharequality !== false) {
quality = session.screensharequality;
}
if (reload) {
// quality = 0, audio = true, videoOnEnd = false) {
await grabScreen(quality, true, true).then(res => {
if (res != false) {
session.screenShareState = true;
pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
notifyOfScreenShare();
getById("screensharebutton").classList.add("green");
getById("screensharebutton").ariaPressed = "true";
enumerateDevices()
.then(gotDevices2)
.then(function () {});
}
});
return;
}
if (session.screenShareState == false) {
// adding a screen
await grabScreen(quality, true, true).then(res => {
if (res != false) {
session.screenShareState = true;
pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
notifyOfScreenShare();
//session.refreshScale();
getById("screensharebutton").classList.add("green");
getById("screensharebutton").ariaPressed = "true";
enumerateDevices()
.then(gotDevices2)
.then(function () {});
//if (session.videoElement.readyState!==4){
session.videoElement.play().then(() => {
log("start play doublecheck");
});
//}
updateMixer();
pokeIframeAPI("screen-share-state", true);
}
});
} else {
// removing a screen . session.screenShareState already true true /////////////////////////////////
session.screenShareState = false;
pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID);
notifyOfScreenShare();
if (!session.streamSrc) {
checkBasicStreamsExist();
}
if (screenShareAudioTrack) {
if (session.videoElement && session.videoElement.srcObject) {
session.videoElement.srcObject.getAudioTracks().forEach(function (track) {
// previous video track; saving it. Must remove the track at some point.
if (screenShareAudioTrack.id == track.id) {
// since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share.
session.videoElement.srcObject.removeTrack(track);
track.stop();
}
});
}
if (session.streamSrcClone) {
//
session.streamSrcClone.getAudioTracks().forEach(function (track) {
if (screenShareAudioTrack.id == track.id) {
// since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share.
session.streamSrcClone.removeTrack(track);
track.stop();
}
});
}
if (session.streamSrc) {
session.streamSrc.getAudioTracks().forEach(function (track) {
// previous video track; saving it. Must remove the track at some point.
if (screenShareAudioTrack.id == track.id) {
// since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share.
session.streamSrc.removeTrack(track);
track.stop();
}
});
}
session.videoElement.srcObject = outboundAudioPipeline(); // updateREnderOoutput is just for video if videoElement is already activated.
screenShareAudioTrack = null;
senderAudioUpdate();
}
var addedAlready = false;
if (session.streamSrc) {
session.streamSrc.getVideoTracks().forEach(function (track) {
if (beforeScreenShare && track.id == beforeScreenShare.id) {
addedAlready = true;
} else {
session.streamSrc.removeTrack(track);
track.stop();
}
});
}
if (session.streamSrcClone) {
session.streamSrcClone.getVideoTracks().forEach(function (track) {
if (beforeScreenShare && track.id == beforeScreenShare.id) {
//
} else {
session.streamSrcClone.removeTrack(track);
track.stop();
}
});
}
if (session.videoElement && session.videoElement.srcObject) {
session.videoElement.srcObject.getVideoTracks().forEach(function (track) {
if (beforeScreenShare && track.id == beforeScreenShare.id) {
addedAlready = true;
} else {
session.videoElement.srcObject.removeTrack(track);
track.stop();
}
});
}
getById("screensharebutton").classList.remove("green");
getById("screensharebutton").ariaPressed = "false";
if (beforeScreenShare) {
if (addedAlready == false) {
session.streamSrc.addTrack(beforeScreenShare); // add back in the video track we had before we started screen sharing. It should be NULL if we changed the video track else where (such as via the settings). #TODO:
}
}
beforeScreenShare = null;
updateRenderOutpipe(); // this syncs the video
toggleSettings(true); // forceShow
updateMixer();
}
}
var ipcRenderer = false;
var ElectronDesktopCapture = false;
var windowAudioCapture = null;
var capturedAudioStream = null;
if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
// this enables Screen Capture in Electron
try {
if (!ipcRenderer) {
ipcRenderer = require("electron").ipcRenderer;
}
// Initialize WindowAudioStream helper
windowAudioCapture = new WindowAudioStream();
window.navigator.mediaDevices.getDisplayMedia = (constraints = false) => {
return new Promise(async (resolve, reject) => {
try {
if (session.autostart) {
session.autostart = false;
if (parseInt(session.screenshare) + "" === session.screenshare) {
var sscid = parseInt(session.screenshare) - 1;
if (sscid < 0) {
sscid = 0;
}
//ipcRenderer.sendSync('prompt', {title, val});
const sources = await ipcRenderer.sendSync("getSources", { types: ["screen"] });
var new_constraints = {
audio: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: sources[sscid].id
}
},
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: sources[sscid].id
}
}
};
if (session.audioDevice === 0) {
new_constraints.audio = false;
}
try {
if (constraints.video.width.ideal) {
new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal;
}
} catch (e) {}
try {
if (constraints.video.height.ideal) {
new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal;
}
} catch (e) {}
try {
if (constraints.video.frameRate.ideal) {
new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal;
}
} catch (e) {}
warnlog("navigator.mediaDevices.getUserMedia starting...");
const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
resolve(stream);
} else if (session.screenshare && session.screenshare !== true) {
var sscid = null;
const sources = await ipcRenderer.sendSync("getSources", { types: ["window"] });
for (var i = 0; i < sources.length; i++) {
if (sources[i].name.toLowerCase().startsWith(session.screenshare.toLowerCase())) {
// check if anythign starts with
sscid = i;
break;
}
}
if (sscid === null) {
sscid = 0; // grab first window if nothing.
for (var i = 0; i < sources.length; i++) {
if (sources[i].name.toLowerCase().includes(session.screenshare.toLowerCase())) {
// check if something includes the string; fallback
sscid = i;
break;
}
}
}
///
var new_constraints = {
audio: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: sources[sscid].id
}
},
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: sources[sscid].id
}
}
};
if (session.audioDevice === 0) {
new_constraints.audio = false;
}
try {
if (constraints.video.width.ideal) {
new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal;
}
} catch (e) {}
try {
if (constraints.video.height.ideal) {
new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal;
}
} catch (e) {}
try {
if (constraints.video.frameRate.ideal) {
new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal;
}
} catch (e) {}
warnlog("navigator.mediaDevices.getUserMedia starting...");
const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
resolve(stream);
} else {
var sscid = 0;
const sources = await ipcRenderer.sendSync("getSources", { types: ["screen"] });
///
var new_constraints = {
audio: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: sources[sscid].id
}
},
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: sources[sscid].id
}
}
};
if (session.audioDevice === 0) {
new_constraints.audio = false;
}
try {
if (constraints.video.width.ideal) {
new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal;
}
} catch (e) {}
try {
if (constraints.video.height.ideal) {
new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal;
}
} catch (e) {}
try {
if (constraints.video.frameRate.ideal) {
new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal;
}
} catch (e) {}
warnlog(new_constraints);
warnlog("navigator.mediaDevices.getUserMedia starting...");
const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints);
resolve(stream);
}
} else {
// Get both sources and window audio
const sources = await ipcRenderer.sendSync("getSources", { types: ["screen", "window"] });
// Get window list with audio capabilities
const windowsWithAudio = await window.electronApi.getWindowList();
// Create a map of window IDs to process IDs for matching
const windowAudioMap = {};
windowsWithAudio.forEach(win => {
windowAudioMap[win.id] = {
processId: win.processId,
title: win.title
};
});
const selectionElem = document.createElement("div");
selectionElem.classList = "desktop-capturer-selection";
if (session.screenshareVideoOnly) {
selectionElem.innerHTML = `
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";
}
}
async function menuItemListener(link, inputElement = false, e = false) {
if (link.getAttribute("data-action") === "Open") {
window.open(taskItemInContext.href);
} else if (link.getAttribute("data-action") === "Copy") {
copyFunction(taskItemInContext.href);
} else if (link.getAttribute("data-action") === "Mirror") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
session.mirrored = !session.mirrored;
applyMirror(false);
log("session.mirrored");
} else {
if ("mirror" in taskItemInContext) {
taskItemInContext.mirror = !taskItemInContext.mirror;
applyMirrorGuest(taskItemInContext.mirror, taskItemInContext);
} else {
taskItemInContext.mirror = true;
applyMirrorGuest(taskItemInContext.mirror, taskItemInContext);
}
}
} else if (link.getAttribute("data-action") === "Rotate") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
session.rotate = ((session.rotate || 0) + 90) % 360;
if (Firefox && session.mobile){
updateForceRotate(true);
} else {
updateForceRotate(false);
}
log("session.rotate");
setTimeout(function () {
updateMixer();
}, 1);
} else {
if ("manualRotate" in taskItemInContext) {
taskItemInContext.manualRotate = ((taskItemInContext.manualRotate || 0) + 90) % 360;
taskItemInContext.rotated = taskItemInContext.manualRotate;
} else {
taskItemInContext.manualRotate = ((taskItemInContext.rotated || 0 ) + 90) % 360;
taskItemInContext.rotated = taskItemInContext.manualRotate;
}
setTimeout(function () {
updateMixer();
}, 1);
}
} else if (link.getAttribute("data-action") === "FullWindow") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
session.infocus = true;
} else {
session.infocus = taskItemInContext.dataset.UUID;
}
updateMixer();
} else if (link.getAttribute("data-action") === "ShrinkWindow") {
session.infocus = false;
updateMixer();
} else if (link.getAttribute("data-action") === "Pause") {
pauseVideo(taskItemInContext);
} else if (link.getAttribute("data-action") === "UnPause") {
unPauseVideo(taskItemInContext);
} else if (link.getAttribute("data-action") === "PiP") {
togglePictureInPicture(taskItemInContext);
} else if (link.getAttribute("data-action") === "PiP2") {
PictureInPicturePageToggle();
} else if (link.getAttribute("data-action") === "Record") {
if (taskItemInContext.stopWriter || taskItemInContext.recording) {
} else if (taskItemInContext.startWriter) {
taskItemInContext.startWriter();
} else {
var videoKbps = session.recordDefault;
if (session.recordLocal !== false) {
videoKbps = session.recordLocal;
}
recordLocalVideo(null, videoKbps, taskItemInContext);
}
} else if (link.getAttribute("data-action") === "StopRecording") {
if (taskItemInContext.stopWriter) {
taskItemInContext.stopWriter();
} else if (taskItemInContext.recording) {
recordLocalVideo("stop", null, taskItemInContext);
}
} else if (link.getAttribute("data-action") === "CopyFrameAsImage") {
copyVideoFrameToClipboard(taskItemInContext, e);
} else if (link.getAttribute("data-action") === "SaveFrameToDisk") {
saveVideoFrameToDisk(taskItemInContext, e);
} else if (link.getAttribute("data-action") === "DrawOnVideo") {
if (taskItemInContext.clearDrawOnVideo){
taskItemInContext.clearDrawOnVideo();
taskItemInContext.clearDrawOnVideo = null;
} else {
taskItemInContext.clearDrawOnVideo = drawOnThis(taskItemInContext);
}
} else if (link.getAttribute("data-action") === "ChangeBuffer") {
toggleBufferSettings(taskItemInContext.dataset.UUID);
} else if (link.getAttribute("data-action") === "Cast") {
//copyFunction(taskItemInContext.href);
} else if (link.getAttribute("data-action") === "Controls") {
//getById("main").classList.add("forcecontrols"); // adds an annoying shadow to the bar area
//taskItemInContext.showControlBar = true;
//checkVideoControlBar(taskItemInContext);
//taskItemInContext.controls = false;
//ele.focus();
taskItemInContext.removeAttribute("controls");
taskItemInContext.setAttribute("controls", "");
taskItemInContext.controls = true;
} else if (link.getAttribute("data-action") === "HideControls") {
//taskItemInContext.showControlBar = false;
taskItemInContext.controls = false;
taskItemInContext.removeAttribute("controls");
} else if (link.getAttribute("data-action") === "Edit") {
//copyFunction(taskItemInContext.href);
var response = await promptAlt("Please note, manual edits to the URL may conflict with the toggles", false, false, taskItemInContext.href);
if (response) {
taskItemInContext.href = response;
taskItemInContext.dataset.raw = response;
taskItemInContext.innerHTML = response;
}
} else if (link.getAttribute("data-action") === "QRCode") {
warnUser("Loading QR Code");
loadQR(function tt(url) {
getById("alertModalMessage").innerHTML = "";
var qrcode = new QRCode(getById("alertModalMessage"), {
width: 300,
height: 300,
colorDark: "#000000",
colorLight: "#FFFFFF",
useSVG: false
});
qrcode.makeCode(url);
getById("alertModalMessage").title = "";
setTimeout(function () {
getById("alertModalMessage").title = "";
if (getById("alertModalMessage").getElementsByTagName("img").length) {
getById("alertModalMessage").getElementsByTagName("img")[0].style.cursor = "none";
}
}, 100);
}, taskItemInContext.href);
} else if (link.getAttribute("data-action") === "ShowStats") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
var [menu, innerMenu] = statsMenuCreator();
menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu);
printMyStats(innerMenu);
} else if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, taskItemInContext.dataset.UUID);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, taskItemInContext.dataset.UUID);
}
} else if (link.getAttribute("data-action") === "OutputAudio") {
enumerateDevices().then(function (deviceInfo) {
var ele = getById(taskItemInContext.id);
var deviceListElement = gotDevices3(deviceInfo, ele);
if (deviceListElement) {
warnUser("Select the audio playback destination for this media:\n\n");
getById("alertModalMessage").appendChild(deviceListElement);
} else {
warnUser("No output devices available");
}
});
//
} else if (link.getAttribute("data-action") === "RemoteHangup") {
if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) {
var confirmHangup = confirm(getTranslation("confirm-disconnect-user"));
if (confirmHangup) {
var msg = {};
msg.hangup = true;
msg.remote = session.remote;
msg = await session.encodeRemote(msg);
session.sendRequest(msg, taskItemInContext.dataset.UUID);
pokeIframeAPI("hungup", "remote", taskItemInContext.dataset.UUID);
}
}
} else if (link.getAttribute("data-action") === "RemoteReload") {
if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) {
var confirmReload = confirm(getTranslation("confirm-reload-user"));
if (confirmReload) {
var msg = {};
msg.reload = true;
msg.remote = session.remote;
msg = await session.encodeRemote(msg);
session.sendRequest(msg, taskItemInContext.dataset.UUID);
pokeIframeAPI("reload", "remote", taskItemInContext.dataset.UUID);
}
}
} else if (link.getAttribute("data-action") === "SSNewTab") {
var URL = "https://" + window.location.hostname + location.pathname + createScreenShareURL(false);
log(URL);
window.open(URL, "_blank").focus();
} else if (link.getAttribute("data-action") === "pip-clock") {
popOutClock(taskItemInContext.children[0]);
} else if (link.getAttribute("data-action") === "Publish") {
var URL = taskItemInContext.href;
URL += "&clean&chroma=000&ssar=landscape&nosettings&prefercurrenttab&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&whippush&whippushtoken";
var win = window.open(URL, "targetWindow", "toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720");
win.focus();
win.resizeTo(1280, 720);
} else if (link.getAttribute("data-action") === "RecordWindow") {
var URL = taskItemInContext.href;
URL += "&clean&chroma=000&ssar=landscape&nosettings&prefercurrenttab&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&autorecordlocal";
var win = window.open(URL, "targetWindow", "toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720");
win.focus();
win.resizeTo(1280, 720);
}
if (inputElement === false) {
log("Task ID - " + taskItemInContext + ", Task action - " + link.getAttribute("data-action"));
toggleMenuOff();
}
}
function menuItemSyncState(menu) {
var items = menu.querySelectorAll("[data-action]");
for (var i = 0; i < items.length; i++) {
if (items[i].getAttribute("data-action") === "FullWindow") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
if (session.infocus === true) {
items[i].parentNode.classList.add("hidden");
} else {
items[i].parentNode.classList.remove("hidden");
}
} else if (taskItemInContext.dataset.UUID === session.infocus) {
items[i].parentNode.classList.add("hidden");
} else {
items[i].parentNode.classList.remove("hidden");
}
} else if (items[i].getAttribute("data-action") === "ShrinkWindow") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
if (session.infocus === true) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (taskItemInContext.dataset.UUID === session.infocus) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "Pause") {
if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) {
if ("prePausedBandwidth" in session.rpcs[taskItemInContext.dataset.UUID]) {
items[i].parentNode.classList.add("hidden");
} else {
items[i].parentNode.classList.remove("hidden");
}
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "UnPause") {
if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) {
if ("prePausedBandwidth" in session.rpcs[taskItemInContext.dataset.UUID]) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "Record") {
if (taskItemInContext.stopWriter || taskItemInContext.recording) {
items[i].parentNode.classList.add("hidden");
} else {
items[i].parentNode.classList.remove("hidden");
}
} else if (items[i].getAttribute("data-action") === "StopRecording") {
if (taskItemInContext.stopWriter || taskItemInContext.recording) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "CopyFrameAsImage") {
if (taskItemInContext.srcObject && taskItemInContext.srcObject.getVideoTracks().length) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "SaveFrameToDisk") {
if (taskItemInContext.srcObject && taskItemInContext.srcObject.getVideoTracks().length) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "Controls") {
if (taskItemInContext.controls) {
items[i].parentNode.classList.add("hidden");
} else {
items[i].parentNode.classList.remove("hidden");
}
} else if (items[i].getAttribute("data-action") === "HideControls") {
if (taskItemInContext.controls) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "PiP2") {
if (typeof documentPictureInPicture !== "undefined") {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "RemoteHangup") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
items[i].parentNode.classList.add("hidden");
} else if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "ChangeBuffer") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
items[i].parentNode.classList.add("hidden");
} else if (session.rpcs[taskItemInContext.dataset.UUID]) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "RemoteReload") {
if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") {
items[i].parentNode.classList.add("hidden");
} else if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "TipRightClick") {
if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
items[i].parentNode.classList.add("hidden");
} else {
items[i].parentNode.classList.remove("hidden");
}
} else if (items[i].getAttribute("data-action") === "Publish") {
if (taskItemInContext.classList.contains("publish")) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
} else if (items[i].getAttribute("data-action") === "RecordWindow") {
if (taskItemInContext.classList.contains("publish")) {
items[i].parentNode.classList.remove("hidden");
} else {
items[i].parentNode.classList.add("hidden");
}
}
}
}
contextListener();
})();
function checkVideoControlBar(ele){ // this is aggressive. Lets not use it unless required.
if (ele){
if (ele.showControlBar){
if (ele.showControlBarInterval){
clearTimeout(ele.showControlBarInterval);
}
ele.focus();
ele.removeAttribute("controls");
ele.setAttribute("controls", "");
ele.focus();
ele.showControlBarInterval = setTimeout(function(ele){
checkVideoControlBar(ele);
}, 100, ele);
}
}
}
function gotDevices3(deviceInfos, vid) {
var audioEle = document.createElement("select");
log(deviceInfos);
if (!deviceInfos.length) {
return false;
}
for (let i = 0; i !== deviceInfos.length; ++i) {
if (deviceInfos[i].kind === "audiooutput") {
var opt = document.createElement("option");
opt.innerText = deviceInfos[i].label;
opt.value = deviceInfos[i].deviceId;
audioEle.appendChild(opt);
audioEle.videoTarget = vid;
if (vid.sinkId) {
if (vid.sinkId == deviceInfos[i].deviceId) {
opt.selected = true;
}
} else if (vid.manualSink) {
if (vid.manualSink == deviceInfos[i].deviceId) {
opt.selected = true;
}
} else if (session.sink) {
if (session.sink == deviceInfos[i].deviceId) {
opt.selected = true;
}
}
}
}
audioEle.onchange = function () {
vid.manualSink = this.options[this.selectedIndex].value;
if (this.videoTarget && this.videoTarget.dataset.UUID) {
session.audioEffects = true;
updateIncomingAudioElement(this.videoTarget.dataset.UUID);
}
resetupAudioOut(this.videoTarget);
};
return audioEle;
}
function popupMessage(e, message = "Copied to Clipboard") {
// right click menu
var posx = 0;
var posy = 0;
if (!e) var e = window.event;
if (e.pageX || e.pageY) {
posx = e.pageX;
posy = e.pageY;
} else if (e.clientX || e.clientY) {
posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
posx += 10;
var menu = getById("messagePopup");
menu.innerHTML = "
" + message + "
";
var menuState = 0;
var menuWidth;
var menuHeight;
var menuPosition;
var menuPositionX;
var menuPositionY;
var windowWidth;
var windowHeight;
if (menuState !== 1) {
menuState = 1;
menu.classList.add("context-menu--active");
}
menuWidth = menu.offsetWidth + 4;
menuHeight = menu.offsetHeight + 4;
windowWidth = window.innerWidth;
windowHeight = window.innerHeight;
if (windowWidth - posx < menuWidth) {
menu.style.left = windowWidth - menuWidth + "px";
} else {
menu.style.left = posx + "px";
}
if (windowHeight - posy < menuHeight) {
menu.style.top = windowHeight - menuHeight + "px";
} else {
menu.style.top = posy + "px";
}
function toggleMenuOff() {
if (menuState !== 0) {
menuState = 0;
menu.classList.remove("context-menu--active");
}
}
menu.classList.remove("fadeout");
var showlength = message.length * 50 || 500;
setTimeout(function () {
menu.classList.add("fadeout");
}, showlength);
setTimeout(function () {
toggleMenuOff();
}, showlength + 1000);
}
function timeSince(date) {
var seconds = Math.floor((new Date() - date) / 1000);
var interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return "Seconds ago";
}
var messageList = [];
function sendChatMessage(chatMsg = false, bc = false) {
// filtered + visual
var data = {};
if (chatMsg === false) {
var msg = document.getElementById("chatInput").value;
} else {
var msg = chatMsg;
}
//msg = sanitizeChat(msg);
if (msg == "") {
return false;
}
msg = convertShortcodes(msg);
var label = "";
if (session.label) {
if (session.director) {
label = "" + session.label + ": ";
} else {
label = "" + session.label + ": ";
}
} else if (session.director) {
label = "Director: ";
}
if (msg.trim() === "/list") {
var listMsg = null;
for (var UUID in session.rpcs) {
if (session.rpcs[UUID].label) {
listMsg = UUID + ": " + session.rpcs[UUID].label;
} else if (session.directorList.indexOf(UUID) >= 0) {
listMsg = UUID + ": Director";
} else {
listMsg = UUID + ": Unknown User";
}
var data = {};
data.msg = listMsg;
data.label = false;
data.type = "alert";
data.time = Date.now();
messageList.push(data);
}
for (var UUID in session.pcs) {
if (UUID in session.rpcs) {
continue;
}
if (session.pcs[UUID].label) {
listMsg = UUID + "; " + session.pcs[UUID].label;
} else if (session.directorList.indexOf(UUID) >= 0) {
listMsg = UUID + "; Director";
} else {
listMsg = UUID + "; Unknown User";
}
var data = {};
data.msg = listMsg;
data.label = false;
data.type = "alert";
data.time = Date.now();
messageList.push(data);
}
if (listMsg === null) {
data.msg = "No other users are connected to you";
data.label = false;
data.type = "alert";
data.time = Date.now();
messageList.push(data);
}
} else if (msg.startsWith("/msg ")) {
var msg = msg.split("/msg ")[1];
msg = msg.split(" ");
uid = msg.shift().toLowerCase();
msg = msg.join(" ");
if (msg == "") {
return false;
}
var sent = false;
for (var UUID in session.rpcs) {
if (UUID.startsWith(uid)) {
sendChat(msg, UUID); // send message to peers
var data = {};
data.time = Date.now();
data.msg = sanitizeChat(msg); // this is what the other person should see
data.label = label;
data.type = "sent";
messageList.push(data);
sent = true;
} else if (session.rpcs[UUID].label && session.rpcs[UUID].label.toLowerCase().startsWith(uid)) {
sendChat(msg, UUID); // send message to peers
var data = {};
data.time = Date.now();
data.msg = sanitizeChat(msg); // this is what the other person should see
data.label = label;
data.type = "sent";
messageList.push(data);
sent = true;
} else if (session.directorList.indexOf(UUID) >= 0 && "director".startsWith(uid)) {
sendChat(msg, UUID); // send message to peers
var data = {};
data.time = Date.now();
data.msg = sanitizeChat(msg); // this is what the other person should see
data.label = label;
data.type = "sent";
messageList.push(data);
sent = true;
}
}
for (var UUID in session.pcs) {
if (UUID in session.rpcs) {
continue;
}
if (UUID.startsWith(uid)) {
sendChat(msg, UUID); // send message to peers
var data = {};
data.time = Date.now();
data.msg = sanitizeChat(msg); // this is what the other person should see
data.label = label;
data.type = "sent";
messageList.push(data);
sent = true;
} else if (session.pcs[UUID].label && session.pcs[UUID].label.toLowerCase().startsWith(uid)) {
sendChat(msg, UUID); // send message to peers
var data = {};
data.time = Date.now();
data.msg = sanitizeChat(msg); // this is what the other person should see
data.label = label;
data.type = "sent";
messageList.push(data);
sent = true;
} else if (session.directorList.indexOf(UUID) >= 0 && "director".startsWith(uid)) {
sendChat(msg, UUID); // send message to peers
var data = {};
data.time = Date.now();
data.msg = sanitizeChat(msg); // this is what the other person should see
data.label = label;
data.type = "sent";
messageList.push(data);
sent = true;
}
}
if (sent == false) {
var data = {};
data.msg = "No user found. Message not sent.";
data.label = false;
data.type = "alert";
data.time = Date.now();
messageList.push(data);
updateMessages();
return false;
}
} else if (msg.startsWith("/")) {
data.msg = "Unknown command. Try '/list' or '/msg username message'.";
data.label = false;
data.type = "alert";
data.time = Date.now();
messageList.push(data);
updateMessages();
return false;
} else if (session.directorChat === true) {
if (session.directorList.length) {
for (var i = 0; i < session.directorList.length; i++) {
sendChat(msg, session.directorList[i]); // send message to peers
}
var data = {};
data.time = Date.now();
data.msg = sanitizeChat(msg); // this is what the other person should see
data.label = label;
data.type = "sent";
messageList.push(data);
}
} else {
sendChat(msg); // send message to peers
data.time = Date.now();
data.msg = sanitizeChat(msg); // this is what the other person should see
data.label = label;
data.type = "sent";
messageList.push(data);
}
document.getElementById("chatInput").value = "";
messageList = messageList.slice(-100);
if (!bc && session.broadcastChannel !== false) {
log(session.broadcastChannel);
session.broadcastChannel.postMessage(data);
}
updateMessages();
if (isIFrame) {
parent.postMessage(
{
chat: data
},
session.iframetarget
);
}
var apiBlob = {};
apiBlob.time = data.time;
apiBlob.msg = msg;
apiBlob.label = session.label;
apiBlob.type = data.type;
pokeAPI("chat", apiBlob);
return true;
}
function disableQualityDirector(UUID) {
// lets revert back to the director's quality settings after viewing the scene
try {
var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.add("disable");
elements[0].ariaPressed = "false";
elements[0].classList.remove("pressed");
elements[0].disabled = "true";
elements[0].title = getTranslation("preview-meshcast-disabled");
}
var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.add("disable");
elements[0].ariaPressed = "false";
elements[0].classList.remove("pressed");
elements[0].disabled = "true";
elements[0].title = getTranslation("preview-meshcast-disabled");
}
var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.add("disable");
elements[0].ariaPressed = "false";
elements[0].classList.remove("pressed");
elements[0].disabled = "true";
elements[0].title = getTranslation("preview-meshcast-disabled");
}
} catch (e) {
errorlog(e);
}
}
function applyQualityDirector(uuid = false) {
// lets revert back to the director's quality settings after viewing the scene
if (uuid) {
var eles = document.querySelectorAll('#guestFeeds button.pressed[data-action-type="change-quality1"][data--u-u-i-d="' + uuid + '"],#guestFeeds button.pressed[data-action-type="change-quality2"][data--u-u-i-d="' + uuid + '"],#guestFeeds button.pressed[data-action-type="change-quality3"][data--u-u-i-d="' + uuid + '"]');
eles.forEach(ele => {
ele.click();
});
} else {
var eles = document.querySelectorAll('#guestFeeds button.pressed[data-action-type="change-quality1"],#guestFeeds button.pressed[data-action-type="change-quality2"],#guestFeeds button.pressed[data-action-type="change-quality3"]');
eles.forEach(ele => {
ele.click();
});
}
}
function toggleQualityDirector(bitrate, UUID, ele) {
// ele is specific to the button in the director's room
var eles = ele.parentNode.childNodes;
for (var i = 0; i < eles.length; i++) {
eles[i].className = "";
}
ele.classList.add("pressed");
ele.ariaPressed = "true";
session.requestRateLimit(bitrate, UUID);
}
var clockOverlayTimer = null;
function zpadTime(number) {
var output = "" + number;
while (output.length < 2) {
output = "0" + output;
}
return output;
}
function showClock() {
getById("overlayClockContainer").classList.remove("hidden");
}
function hideClock() {
getById("overlayClockContainer").classList.add("hidden");
}
function setClock(initial = false, color = "#000") {
if (initial !== false) {
initial = parseInt(initial);
getById("overlayClockContainer").dataset.initial = initial;
} else {
initial = parseInt(getById("overlayClockContainer").dataset.initial);
}
if (initial < 0) {
initial = 0;
}
updateClock(initial, color);
}
function stopClock() {
var clock = document.getElementById("overlayClock");
//clock.ctx = null;
//clock.canvas = null;
//if (document.pictureInPictureElement && clock.video) {
// if (document.pictureInPictureElement == clock.video){
// document.exitPictureInPicture();
// pokeIframeAPI('picture-in-picture', false);
// }
//clock.video.remove;
//}
clock.innerHTML = "";
clearInterval(clockOverlayTimer);
//setClock();
updateClock("0", "#444");
}
function pauseClock() {
clearInterval(clockOverlayTimer);
var current = Date.now() - parseInt(getById("overlayClockContainer").dataset.timestamp);
if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) {
current = parseInt(Math.round(current / 1000));
} else {
current = parseInt(getById("overlayClockContainer").dataset.initial) - parseInt(Math.round(current / 1000));
}
getById("overlayClockContainer").dataset.current = current;
updateClock(current, "#00F");
}
function resumeClock() {
if ("current" in getById("overlayClockContainer").dataset) {
startClock(parseInt(getById("overlayClockContainer").dataset.current));
}
}
function startClock(restart = true) {
clearInterval(clockOverlayTimer);
if (restart === true) {
getById("overlayClockContainer").dataset.timestamp = Date.now();
} else if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) {
getById("overlayClockContainer").dataset.timestamp = Date.now() - parseInt(getById("overlayClockContainer").dataset.current * 1000);
} else {
getById("overlayClockContainer").dataset.timestamp = Date.now() - (parseInt(getById("overlayClockContainer").dataset.initial) * 1000 - parseInt(getById("overlayClockContainer").dataset.current * 1000));
}
stepClock();
var clock = document.getElementById("overlayClock");
if (clock && clock.video) {
clock.innerHTML = "";
clock.appendChild(clock.video);
clock.video.play();
}
clockOverlayTimer = setInterval(function () {
stepClock();
}, 999);
}
function stepClock() {
var current = Date.now() - parseInt(getById("overlayClockContainer").dataset.timestamp);
if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) {
current = parseInt(Math.round(current / 1000));
} else {
current = parseInt(getById("overlayClockContainer").dataset.initial) - parseInt(Math.round(current / 1000));
}
if (session.directorList.length) {
var msg = {};
msg.timer = current;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
}
if (current < 0 && current % 2) {
updateClock(0, "#F00");
} else if (current < 0) {
updateClock(0, "#000");
} else {
updateClock(current, "#000");
}
}
function updateClock(timeleft, color = "#000") {
var minutes = Math.floor(timeleft / 60);
var seconds = timeleft % 60;
var clock = document.getElementById("overlayClock");
if (clock.ctx) {
clock.ctx.beginPath();
clock.ctx.rect(0, 0, 230, 40);
clock.ctx.fillStyle = color;
clock.ctx.fill();
clock.ctx.fillStyle = "#FFF";
clock.ctx.textAlign = "center";
clock.ctx.font = "50px monospace";
clock.ctx.fillText(zpadTime(minutes) + ":" + zpadTime(seconds), 115, 37);
} else {
clock.innerHTML = zpadTime(minutes) + ":" + zpadTime(seconds);
clock.style.backgroundColor = color + "9";
}
}
function popOutClock(clock) {
if (!clock.ctx) {
var canvas = document.createElement("canvas");
canvas.width = "230";
canvas.height = "40";
var ctx = canvas.getContext("2d");
clock.canvas = canvas;
clock.ctx = ctx;
ctx.beginPath();
ctx.rect(0, 0, 230, 40);
ctx.fillStyle = "#000";
ctx.fill();
ctx.fillStyle = "#FFF";
ctx.font = "50px monospace";
ctx.textAlign = "center";
ctx.fillText(clock.innerHTML, 115, 37);
clock.video = document.createElement("video");
clock.innerHTML = "";
clock.appendChild(clock.video);
clock.video.onloadedmetadata = function () {
togglePictureInPicture(clock.video);
};
clock.video.srcObject = canvas.captureStream();
clock.video.play();
//clock.video.dataset.menu = "context-menu-clock";
} else {
clock.innerHTML = "";
clock.appendChild(clock.video);
clock.video.play();
togglePictureInPicture(clock.video);
}
}
session.popupChat = null
async function createPopoutChat() {
if (session.popupChat && !session.popupChat.closed) {
session.popupChat.focus();
return;
}
if (session.broadcastChannelID === false) {
session.broadcastChannelID = session.generateStreamID(8);
log(session.broadcastChannelID);
session.broadcastChannel = new BroadcastChannel(session.broadcastChannelID);
session.broadcastChannel.onmessage = function (e) {
if ("loaded" in e.data) {
session.broadcastChannel.postMessage({
messageList: messageList
});
} else if ("msg" in e.data) {
sendChatMessage(e.data.msg, true);
}
};
session.broadcastChannel.onmessageerror = function (e) {
errorlog(e);
};
}
let params = {
broadcastChannelID: session.broadcastChannelID,
room: session.roomid || false,
view: session.view_set ? [...session.view_set,session.streamID].join(",") : (session.roomid ? false : session.streamID),
label: session.label || false,
password: session.password
};
function encrypt(text, key) {
const textEncoder = new TextEncoder();
const encodedText = textEncoder.encode(text);
const encodedKey = textEncoder.encode(key);
const encrypted = encodedText.map((byte, i) =>
byte ^ encodedKey[i % encodedKey.length]
);
return btoa(String.fromCharCode.apply(null, encrypted));
}
async function generateSecureUrl(params) {
const ENCRYPTION_KEY = 'your32characterlongencryptionkey!!';
const filteredParams = Object.fromEntries(
Object.entries(params).filter(([_, v]) => v != null && v !== undefined)
);
const paramsString = JSON.stringify(filteredParams);
const encrypted = encrypt(paramsString, ENCRYPTION_KEY);
return `./popout.html?id=${session.broadcastChannelID}&data=${encodeURIComponent(encrypted)}`;
}
let srcString = await generateSecureUrl(params);
log(srcString);
session.popupChat = window.open(srcString, "popup", "width=600,height=480,toolbar=no,menubar=no,resizable=yes");
session.popupChat.document.body.style.margin = "0";
session.popupChat.document.body.style.backgroundColor = "#000";
session.popupChat.document.body.style.padding = "0";
session.popupChat.document.body.style.overflow = "hidden";
session.popupChat.document.title = "Chat pop-out";
const style = session.popupChat.document.createElement('style');
style.textContent = `
@keyframes pulse {
0% { background-color: #000; }
50% { background-color: #333; }
100% { background-color: #000; }
}
body {
animation: pulse 2s ease-in-out infinite;
}
`;
session.popupChat.document.head.appendChild(style);
return false;
}
function replaceURLs(message) {
if (!message) return;
var urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
return message.replace(urlRegex, function (url) {
url = url.replace(//g, ">").replace(/["']/g, ""); // try to sanitize things, just in case.
var punc = "";
while (url[url.length - 1] === ".") {
url = url.slice(0, -1);
punc += ".";
}
while (url[url.length - 1] === ";") {
url = url.slice(0, -1);
punc += ";";
}
while (url[url.length - 1] === ",") {
url = url.slice(0, -1);
punc += ",";
}
while (url[url.length - 1] === "!") {
url = url.slice(0, -1);
punc += "!";
}
while (url[url.length - 1] === ":") {
url = url.slice(0, -1);
punc += ":";
}
while (url[url.length - 1] === "*") {
url = url.slice(0, -1);
punc += "*";
}
while (url[url.length - 1] === ")") {
url = url.slice(0, -1);
punc += ")";
}
while (url[url.length - 1] === "?") {
url = url.slice(0, -1);
punc += "?";
}
var hyperlink = url;
if (!hyperlink.match("^https?://")) {
hyperlink = "http://" + hyperlink;
}
if (url.length > 35) {
url = url.substring(0, 35) + "...";
}
return '' + url + "" + punc;
});
}
function getChatMessage(msg, label = false, director = false, overlay = false, UUID = false) {
msg = sanitizeChat(msg); // keep it clean.
if (msg == "") {
return;
}
data = {};
data.time = Date.now();
var apiBlob = {};
apiBlob.time = data.time;
apiBlob.msg = msg;
apiBlob.label = label;
apiBlob.type = "recv";
if (UUID !== false) {
apiBlob.UUID = UUID;
if (UUID in session.rpcs) {
apiBlob.streamID = session.rpcs[UUID].streamID || false;
}
}
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) {
data.label = "Someone: ";
} else {
data.label = "";
}
label = "";
}
data.type = "recv";
if (overlay) {
if (!(session.cleanOutput && session.cleanish == false)) {
var textOverlay = getById("overlayMsgs");
if (textOverlay) {
if (overlay == 2) {
// Clear previous persistent messages
while (textOverlay.querySelector('.persistent-overlay')) {
textOverlay.removeChild(textOverlay.querySelector('.persistent-overlay'));
}
var spanOverlay = document.createElement("span");
spanOverlay.className = 'persistent-overlay';
spanOverlay.innerHTML = "" + label + " " + msg + " ";
textOverlay.appendChild(spanOverlay);
textOverlay.style.display = "block";
} else {
var spanOverlay = document.createElement("span");
spanOverlay.innerHTML = "" + label + " " + msg + " ";
textOverlay.appendChild(spanOverlay);
textOverlay.style.display = "block";
var showtime = msg.length * 200 + 3000;
if (showtime > 8000) {
showtime = 8000;
}
setTimeout(
function (ele) {
try {
ele.parentNode.removeChild(ele);
} catch (e) {}
},
showtime,
spanOverlay
);
}
}
}
}
if (isIFrame) {
parent.postMessage(
{
gotChat: data, // deprecated
chat: data
},
session.iframetarget
);
}
pokeAPI("chat", apiBlob);
if (session.chatbutton === false) {
return;
} // messages can still appear as overlays ^
messageList.push(data);
messageList = messageList.slice(-100);
if (session.beepToNotify) {
playtone();
showNotification("new message", msg);
}
updateMessages();
if (session.chat == false) {
getById("chattoggle").className = "las la-comments toggleSize pulsate";
getById("chatbutton").className = "float";
if (getById("chatNotification").value) {
getById("chatNotification").value = getById("chatNotification").value + 1;
} else {
getById("chatNotification").value = 1;
}
getById("chatNotification").classList.add("notification", "red");
}
if (session.broadcastChannel !== false) {
session.broadcastChannel.postMessage(data); /* send */
}
}
function rainbow(step, colours) {
var r, g, b;
var h = 1 - step / colours;
var i = ~~(h * 6);
var f = h * 6 - i;
var q = 1 - f;
switch (i % 6) {
case 0:
(r = 1), (g = f), (b = 0);
break;
case 1:
(r = q), (g = 1), (b = 0);
break;
case 2:
(r = 0), (g = 1), (b = f);
break;
case 3:
(r = 0), (g = q), (b = 1);
break;
case 4:
(r = f), (g = 0), (b = 1);
break;
case 5:
(r = 1), (g = 0), (b = q);
break;
}
var c = "#" + ("00" + (~~(r * 200 + 35)).toString(16)).slice(-2) + ("00" + (~~(g * 200 + 35)).toString(16)).slice(-2) + ("00" + (~~(b * 200 + 35)).toString(16)).slice(-2);
return c;
}
function getColorFromName(str, colorseed = false, totalcolors = false) {
var out = 0,
len = str.length;
if (len > 6) {
len = 6;
}
var seed = 26;
if (colorseed) {
seed = colorseed || 1;
}
for (var pos = 0; pos < len; pos++) {
out += (str.charCodeAt(pos) - 64) * Math.pow(seed, len - pos - 1);
}
var colours = 167772;
if (totalcolors) {
colours = totalcolors;
if (colours > 167772) {
colours = 167772;
} else if (colours < 1) {
colours = 1;
}
}
out = parseInt(out % colours); // get modulus
if (colours === 1) {
return "#F00";
} else if (colours === 2) {
switch (out) {
case 0:
return "#F00";
case 1:
return "#00ABFA";
}
} else if (colours === 3) {
switch (out) {
case 0:
return "#F00";
case 1:
return "#00A800";
case 2:
return "#00ABFA";
}
} else if (colours === 4) {
switch (out) {
case 0:
return "#F00";
case 1:
return "#FFA500";
case 2:
return "#00A800";
case 3:
return "#00ABFA";
}
} else if (colours === 5) {
switch (out) {
case 0:
return "#F00";
case 1:
return "#FFA500";
case 2:
return "#00A800";
case 3:
return "#00ABFA";
case 4:
return "#FF39C5";
}
} else {
out = rainbow(out, colours);
}
return out;
}
function updateClosedCaptions(msg, label, UUID) {
if (!session.rpcs[UUID].color && session.ccColored) {
session.rpcs[UUID].color = getColorFromName(UUID);
}
msg.counter = parseInt(msg.counter);
var temp = document.createElement("div");
temp.innerText = msg.transcript;
temp.innerText = temp.innerHTML;
var transcript = temp.textContent || temp.innerText || "";
if (transcript == "") {
return;
}
transcript = transcript.charAt(0).toUpperCase() + transcript.slice(1);
//transcript = transcript.substr(-1, 5000); // keep it from being too long
if (session.nocaptionlabels) {
label = "";
} else if (label && !(session.view && !session.view_set)) {
label = sanitizeLabel(label);
label = "" + label + ": ";
} else {
label = "";
}
var textOverlay = getById("overlayMsgs");
if (textOverlay) {
if (document.getElementById(UUID + "_" + msg.counter)) {
var spanOverlay = document.getElementById(UUID + "_" + msg.counter);
} else {
var spanOverlay = document.createElement("span");
spanOverlay.id = UUID + "_" + msg.counter;
textOverlay.appendChild(spanOverlay);
textOverlay.style.height = "unset";
textOverlay.style.textAlign = "left";
textOverlay.style.display = "block";
textOverlay.style.position = "fixed";
textOverlay.style.bottom = "0";
}
spanOverlay.innerHTML = label + transcript + " ";
spanOverlay.style.fontSize = (parseInt(session.labelsize || 100) / 100.0) * 4.5 + "vh";
spanOverlay.style.lineHeight = (parseInt(session.labelsize || 100) / 100) * 6 + "vh";
spanOverlay.style.margin = (parseInt(session.labelsize || 100) / 100.0) * 0.75 + "vh";
if (session.rpcs[UUID].color && session.ccColored) {
spanOverlay.style.color = session.rpcs[UUID].color;
}
if (msg.isFinal) {
var showtime = 3000;
clearTimeout(spanOverlay.timeout);
spanOverlay.timeout = setTimeout(
function (ele) {
ele.parentNode.removeChild(ele);
},
showtime,
spanOverlay
);
} else {
clearTimeout(spanOverlay.timeout);
spanOverlay.timeout = setTimeout(
function (ele) {
ele.parentNode.removeChild(ele);
},
30000,
spanOverlay
);
}
}
}
var chatUpdateTimeout = null;
function updateMessages() {
if (session.chatbutton === false) {
return;
}
getById("chatNotification").classList.remove("notification", "red");
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 {
msg.innerHTML = message;
msg.classList.add("outMessage");
}
chatBody.appendChild(msg);
}
showDownloadLinks();
for (var i in msgTransferList) {
var time = timeSince(msgTransferList[i].time) || "";
time = " - " + time + "";
var msg = document.createElement("div");
if ("idx" in msgTransferList[i]) {
msg.id = "transfer_" + msgTransferList[i].idx;
msg.classList.add("transfer");
}
if (msgTransferList[i].type == "sent") {
msg.innerHTML = msgTransferList[i].msg + "" + time + "";
msg.classList.add("outMessage");
} else if (msgTransferList[i].type == "recv" || msgTransferList[i].type == "action") {
var label = "";
if (msgTransferList[i].label) {
label = msgTransferList[i].label;
}
msg.innerHTML = label + msgTransferList[i].msg + "" + time + "";
msg.classList.add("inMessage");
} else if (msgTransferList[i].type == "alert") {
msg.innerHTML = msgTransferList[i].msg + "" + time + "";
msg.classList.add("inMessage");
} else {
msg.innerHTML = msgTransferList[i].msg;
msg.classList.add("outMessage");
}
if (msg.id && document.getElementById(msg.id)) {
document.getElementById(msg.id).innerHTML = msg.innerHTML;
} else {
chatBody.appendChild(msg);
}
}
if (chatUpdateTimeout) {
clearInterval(chatUpdateTimeout);
}
chatBody.scrollTop = chatBody.scrollHeight;
if (chatUpdateTimeout) {
clearTimeout(chatUpdateTimeout);
}
chatUpdateTimeout = setTimeout(updateMessages, 60000);
}
function EnterButtonChat(event) {
// Number 13 is the "Enter" key on the keyboard
var key = event.which || event.keyCode;
if (key === 13) {
// Cancel the default action, if needed
event.preventDefault();
// Trigger the button element with a click
sendChatMessage();
}
}
function showCustomizer(arg, ele) {
//getById("directorLinksButton").innerHTML=' LINKS (GUEST INVITES & SCENES)'
getById("showCustomizerButton1").style.backgroundColor = "";
getById("showCustomizerButton2").style.backgroundColor = "";
getById("showCustomizerButton3").style.backgroundColor = "";
getById("showCustomizerButton4").style.backgroundColor = "";
getById("showCustomizerButton1").style.boxShadow = "";
getById("showCustomizerButton2").style.boxShadow = "";
getById("showCustomizerButton3").style.boxShadow = "";
getById("showCustomizerButton4").style.boxShadow = "";
if (getById("customizeLinks" + arg).style.display != "none") {
getById("customizeLinks").style.display = "none";
getById("customizeLinks" + arg).style.display = "none";
} else {
//directorLinks").style.display="none";
getById("showCustomizerButton" + arg).style.backgroundColor = "#1e0000";
getById("showCustomizerButton" + arg).style.boxShadow = "inset 0px 0px 1px #b90000";
getById("customizeLinks1").style.display = "none";
getById("customizeLinks3").style.display = "none";
getById("customizeLinks").style.display = "block";
getById("customizeLinks" + arg).style.display = "block";
}
}
function setPTTvalue() {
var key = "";
if (PPTHotkey.ctrl) {
key += "Control";
}
if (PPTHotkey.meta) {
if (key) {
key += " + ";
}
key += "Meta";
}
if (PPTHotkey.alt) {
if (key) {
key += " + ";
}
key += "Alt";
}
if (PPTHotkey.key == "Control") {
//
} else if (PPTHotkey.key == "Alt") {
//
} else if (PPTHotkey.key == "Meta") {
//
} else if (PPTHotkey.key !== false) {
if (key) {
key += " + ";
}
if (PPTHotkey.key === " ") {
key += "Space";
} else {
key += PPTHotkey.key;
}
} else if (key && navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
getById("pptHotKey").title = "Note: Global hot-keys can't simply be Control, Alt, or Meta keys.";
}
getById("pptHotKey").value = key;
try {
if (window.electronApi && window.electronApi.updatePPT) {
window.electronApi.updatePPT(PPTHotkey);
} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
if (!ipcRenderer) {
ipcRenderer = require("electron").ipcRenderer;
}
if (ipcRenderer) {
ipcRenderer.send("PPTHotkey", PPTHotkey);
}
}
} catch (e) {
errorlog(e);
}
}
var PPTHotkey = getStorage("PPTHotkey") || false;
if (PPTHotkey) {
setPTTvalue();
}
function setHotKeyAuto(hotkeyInput) {
PPTHotkey = {
ctrl: false,
alt: false,
meta: false,
key: false
};
var key = "";
if (hotkeyInput) {
const modifiers = hotkeyInput.replaceAll(" ", "+").split("+");
modifiers.forEach(modifier => {
const trimmedModifier = modifier.trim().toLowerCase();
if (trimmedModifier === "control") {
PPTHotkey.ctrl = true;
key += "Control";
} else if (trimmedModifier === "ctrl") {
PPTHotkey.ctrl = true;
key += "Control";
} else if (trimmedModifier === "alt") {
PPTHotkey.alt = true;
key += "Alt";
} else if (trimmedModifier === "meta") {
PPTHotkey.meta = true;
key += "Meta";
}
});
var lastKey = modifiers.pop().trim();
PPTHotkey.key = lastKey;
if (lastKey || lastKey === " " || lastKey === 0) {
if (key) {
key += " + ";
}
if (lastKey === " ") {
key += "Space";
} else {
key += lastKey;
}
}
} else {
PPTHotkey.ctrl = true;
PPTHotkey.key = "m";
PPTHotkey.meta = true;
key = "Control + Alt + m";
}
setStorage("PPTHotkey", PPTHotkey, 99999);
getById("pptHotKey").value = key;
try {
if (window.electronApi && window.electronApi.updatePPT) {
window.electronApi.updatePPT(PPTHotkey);
} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
if (!ipcRenderer) {
ipcRenderer = require("electron").ipcRenderer;
}
if (ipcRenderer) {
ipcRenderer.send("PPTHotkey", PPTHotkey);
}
}
} catch (e) {
errorlog(e);
}
}
function setHotKey(keyinput = true) {
if (!keyinput) {
// clears if false
getById("pptHotKey").value = "";
getById("pptHotKey0").value = "";
PPTHotkey = false;
removeStorage("PPTHotkey");
try {
if (window.electronApi && window.electronApi.updatePPT) {
window.electronApi.updatePPT(PPTHotkey);
} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
if (!ipcRenderer) {
ipcRenderer = require("electron").ipcRenderer;
}
if (ipcRenderer) {
ipcRenderer.send("PPTHotkey", PPTHotkey);
}
}
} catch (e) {
errorlog(e);
}
return;
}
PPTHotkey = {
ctrl: false,
alt: false,
meta: false,
key: false
};
log(event);
var key = "";
if (event.ctrlKey) {
key += "Control";
PPTHotkey.ctrl = true;
}
if (event.metaKey) {
if (key) {
key += " + ";
}
key += "Meta";
PPTHotkey.meta = true;
}
if (event.altKey) {
if (key) {
key += " + ";
}
key += "Alt";
PPTHotkey.alt = true;
}
if (event.key == "Control") {
//
} else if (event.key == "Alt") {
//
} else if (event.key == "Meta") {
//
} else if (event.key || event.key === " " || event.key === 0) {
if (key) {
key += " + ";
}
if (event.key === " ") {
key += "Space";
} else {
key += event.key;
}
PPTHotkey.key = event.key;
}
setStorage("PPTHotkey", PPTHotkey, 99999);
event.target.value = key;
try {
if (window.electronApi && window.electronApi.updatePPT) {
window.electronApi.updatePPT(PPTHotkey);
} else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) {
if (!ipcRenderer) {
ipcRenderer = require("electron").ipcRenderer;
}
if (ipcRenderer) {
ipcRenderer.send("PPTHotkey", PPTHotkey);
}
}
} catch (e) {
errorlog(e);
}
getById("pptHotKey").value = event.target.value;
getById("pptHotKey0").value = event.target.value;
event.preventDefault();
event.stopPropagation();
return false;
}
function setupGoogleDriveUploader(filename = false, sessionUri = false) {
if (!session.gdrive) {
session.gdrive = {};
session.gdrive.accessToken = false;
}
var gdrive = {};
var uploading = false;
var tokenClient;
var isInitialized = false;
var initializationPromise;
const SCOPES = "https://www.googleapis.com/auth/drive.file";
var totalChunksRecorded = 0;
var totalChunksUploaded = 0;
var currentByte = 0;
var chunks = new Blob([]);
var finalized = false;
gdrive.promise = false;
gdrive.sessionUri = sessionUri;
// Create an initialization promise to track when everything is ready
initializationPromise = new Promise((resolve, reject) => {
// We'll resolve this when the token client is fully initialized
if (!gdrive.sessionUri) {
loadScript("https://accounts.google.com/gsi/client", function() {
log("Google Identity Services loaded");
initTokenClient();
resolve();
});
} else {
resolve();
}
});
// Setup the authentication promise
if (!filename && !sessionUri) {
var res, rej;
gdrive.promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
gdrive.promise.resolve = res;
gdrive.promise.reject = rej;
}
gdrive.startResumableUpload = async function(fname, retry = true) {
console.log("startResumableUpload", retry);
const fileMetadata = { name: fname };
if (session.GDRIVE_FOLDERNAME) {
let folderId = null;
const query = `name = '${session.GDRIVE_FOLDERNAME}' and mimeType = 'application/vnd.google-apps.folder' and 'root' in parents and trashed = false`;
const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: "Bearer " + session.gdrive.accessToken
}
});
const result = await response.json();
if (result.files && result.files.length > 0) {
folderId = result.files[0].id;
}
if (!folderId) {
log("creating new folder as folder not found.");
try {
const folderMetadata = {
name: session.GDRIVE_FOLDERNAME,
mimeType: "application/vnd.google-apps.folder"
};
const createResponse = await fetch("https://www.googleapis.com/drive/v3/files", {
method: "POST",
headers: {
Authorization: "Bearer " + session.gdrive.accessToken,
"Content-Type": "application/json"
},
body: JSON.stringify(folderMetadata)
});
const createResult = await createResponse.json();
folderId = createResult.id;
} catch (e) {
errorlog(e);
}
}
if (folderId) {
fileMetadata.parents = [folderId];
}
}
const metadata = new Blob([JSON.stringify(fileMetadata)], { type: "application/json" });
try {
var response = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable", {
method: "POST",
headers: {
Authorization: "Bearer " + session.gdrive.accessToken,
"Content-Type": "application/json; charset=UTF-8"
},
body: metadata
});
if (!response.ok) {
if (!session.cleanOutput) {
warnUser("⚠️ Error: Failed to configure the Google Drive upload.");
}
throw new Error("Start resumable upload failed: " + response.statusText);
}
return response.headers.get("Location"); // This is the session URI for the resumable upload
} catch (err) {
errorlog(err);
try {
if (retry) {
session.gdrive.accessToken = false;
var res, rej;
gdrive.promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
gdrive.promise.resolve = res;
gdrive.promise.reject = rej;
filename = false;
// Make sure we're initialized before requesting token
await gdrive.ensureInitialized();
tokenClient.requestAccessToken();
await gdrive.promise;
if (session.gdrive.accessToken) {
return await gdrive.startResumableUpload(fname, false);
} else {
return false;
}
}
} catch (err2) {
errorlog(err2);
return false;
}
}
};
function initTokenClient() {
console.log("Initializing GIS token client");
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: session.GDRIVE_CLIENT_ID,
scope: SCOPES,
callback: onTokenResponse,
error_callback: onTokenError
});
isInitialized = true;
// If we have no promise yet but the user requested access, set one up
if (!gdrive.promise && !sessionUri && !filename) {
var res, rej;
gdrive.promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
gdrive.promise.resolve = res;
gdrive.promise.reject = rej;
}
// If we have a filename, request token automatically
if (filename) {
console.log("Requesting access token for immediate upload");
setTimeout(() => {
tokenClient.requestAccessToken();
}, 500); // Small delay to ensure tokenClient is fully initialized
}
}
function onTokenError(response) {
console.warn("Token error:", response);
if (gdrive.promise && gdrive.promise.reject) {
gdrive.promise.reject(response);
}
}
async function onTokenResponse(tokenResponse) {
console.log("Token response received", tokenResponse);
if (tokenResponse.error === "popup_closed_by_user" || tokenResponse.error === "access_denied") {
errorlog("User cancelled the sign-in process.");
if (gdrive.promise && gdrive.promise.reject) {
gdrive.promise.reject(new Error("User cancelled authentication"));
}
} else if (tokenResponse.error !== undefined) {
errorlog("Token error: " + tokenResponse.error);
if (gdrive.promise && gdrive.promise.reject) {
gdrive.promise.reject(new Error(tokenResponse.error));
}
} else {
// Successfully got access token
console.log("Access token obtained successfully");
session.gdrive.accessToken = tokenResponse.access_token;
if (filename) {
try {
gdrive.sessionUri = await gdrive.startResumableUpload(filename);
console.log("Session URI:", gdrive.sessionUri);
uploadLoop();
} catch (e) {
console.error("Error starting upload:", e);
if (gdrive.promise && gdrive.promise.reject) {
gdrive.promise.reject(e);
}
return;
}
}
// Always resolve the promise if we got a token successfully
if (gdrive.promise && gdrive.promise.resolve) {
console.log("Resolving promise with access token");
gdrive.promise.resolve(tokenResponse.access_token);
}
}
}
// Check if initialized and wait if not
gdrive.ensureInitialized = async function() {
if (!isInitialized) {
console.log("Waiting for initialization to complete...");
await initializationPromise;
console.log("Initialization complete");
}
};
// Function to manually request access token
gdrive.requestAccessToken = async function() {
await gdrive.ensureInitialized();
if (tokenClient) {
console.log("Manually requesting access token");
tokenClient.requestAccessToken();
} else {
console.error("Token client not initialized");
if (gdrive.promise && gdrive.promise.reject) {
gdrive.promise.reject(new Error("Token client not initialized"));
}
}
};
gdrive.revokeToken = function() {
if (session.gdrive.accessToken) {
google.accounts.oauth2.revoke(session.gdrive.accessToken, () => {
console.log('Access token revoked');
session.gdrive.accessToken = false;
});
}
};
/// the following doesn't need to be signed in; just access to the gdrive.sessionUri URL
gdrive.addChunk = function(chunk) {
if (chunk && chunks) {
totalChunksRecorded += chunk.size;
chunks = new Blob([chunks, chunk], { type: chunk.type });
if (!session.cleanOutput) {
getById("progressContainer").classList.remove("hidden");
}
updateProgressBar();
} else if (chunk === false) {
finalized = true;
}
uploadLoop();
};
async function uploadLoop() {
if (uploading || !gdrive.sessionUri) {
return;
}
uploading = true;
while (chunks && (finalized || chunks.size > 256 * 1024)) {
if (finalized) {
var chunk = chunks.slice(0, chunks.size);
let res = await finalizeUpload(chunk);
log(res);
return;
} else {
var chunkSize = Math.floor(chunks.size / (256 * 1024)) * (256 * 1024);
var chunk = chunks.slice(0, chunkSize);
chunks = chunks.slice(chunkSize);
}
currentByte = await uploadChunk(chunk);
}
uploading = false;
}
async function uploadChunk(chunk) {
const endByte = currentByte + chunk.size - 1;
totalChunksUploaded += chunk.size;
const headers = new Headers({
"Content-Range": `bytes ${currentByte}-${endByte}/*`
});
const response = await fetch(gdrive.sessionUri, {
method: "PUT",
headers: headers,
body: chunk
});
if (!response.ok && response.status !== 308) {
throw new Error(`Failed to upload chunk: ${response.statusText}`);
}
updateProgressBar();
return endByte + 1;
}
async function finalizeUpload(chunk) {
const endByte = currentByte + chunk.size - 1;
const headers = new Headers({
"Content-Range": `bytes ${currentByte}-${endByte}/${endByte + 1}`
});
const response = await fetch(gdrive.sessionUri, {
method: "PUT",
headers: headers,
body: chunk
});
if (chunk) {
totalChunksUploaded += chunk.size;
}
updateProgressBar(2);
return response.json();
}
function updateProgressBar(state = 0) {
// Implementation unchanged
if (state == 2) {
setTimeout(function() {
if (getById("progressBar").style.width == "100%") {
getById("progressContainer").classList.add("hidden");
}
}, 1000);
getById("progressBar").style.width = "100%";
var msg = {};
msg.gdrive = { up: parseInt(totalChunksUploaded / 1024), rec: parseInt(totalChunksUploaded / 1024), state: state };
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
} else if (totalChunksRecorded > 0) {
var progressPercentage = (totalChunksUploaded / (totalChunksRecorded || 1)) * 100;
var bytesLeft = parseInt((totalChunksRecorded - totalChunksUploaded) / 1024);
getById("progressBar").style.width = progressPercentage + "%";
getById("progressBar").innerHTML = "Upload progress to Google Drive: " + progressPercentage.toFixed(2) + "%, with " + convertKilobytes(bytesLeft) + " left";
var msg = {};
msg.gdrive = { up: parseInt(totalChunksUploaded / 1024), rec: parseInt(totalChunksRecorded / 1024), state: state };
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
}
}
return gdrive;
}
function convertKilobytes(kilobytes) {
const KB_IN_MB = 1024;
const KB_IN_GB = 1024 * 1024;
if (kilobytes >= KB_IN_GB) {
return Math.ceil(kilobytes / KB_IN_GB).toFixed(0) + " GB";
} else if (kilobytes >= KB_IN_MB) {
return Math.ceil(kilobytes / KB_IN_MB).toFixed(0) + " MB";
} else {
return kilobytes + "KB";
}
}
function resumeDropbox() {
var sessionData = localStorage.getItem("dropboxSession");
if (sessionData) {
sessionData = JSON.parse(sessionData);
sessionData.forEach(main => {
session.dbx
.filesUploadSessionFinish({ cursor: { session_id: main.result.session_id, offset: main.vdo.offset }, commit: { path: "/" + main.vdo.filename } })
.then(function (response) {
console.log(response);
console.log("File uploaded to Dropbox:", response.result.path_display);
DBXqueue = [];
//localStorage.removeItem('dropboxSession');
})
.catch(function (error) {
localStorage.removeItem("dropboxSession");
console.error("Error uploading file:", error);
if (!session.cleanOutput) {
confirmAlt("There was an error finalizing a previous file upload. \n" + (error.error_summary || "") + "\n\nWould you like to keep trying?").then(res => {
if (!res) {
localStorage.removeItem("dropboxSession");
}
});
}
});
});
}
}
async function streamVideoToDropbox(filename) {
if (!session.dbx) {
if (session.directorUUID) {
var msg = {};
msg.dropbox = -2;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendPeers(msg, msg.UUID);
}
}
return;
}
return await session.dbx
.filesUploadSessionStart({ close: false })
.then(function (main) {
var sessionId = main.result.session_id;
var offset = 0;
var chunkCounter = 0;
var DBXqueue = [];
var uploadActive = false;
var done;
log(main);
main.vdo = {};
main.vdo.filename = filename;
main.vdo.offset = offset;
var sessionData = localStorage.getItem("dropboxSession");
if (sessionData) {
try {
sessionData = JSON.parse(sessionData);
} catch (e) {
errorlog(e);
sessionData = [];
}
sessionData.push(main);
} else {
sessionData = [main];
}
localStorage.setItem("dropboxSession", JSON.stringify(sessionData));
if (session.directorUUID) {
var msg = {};
msg.dropbox = -1;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendPeers(msg, msg.UUID);
}
}
var totalChunksRecorded = 0;
var totalChunksUploaded = 0;
function updateTotalChunksRecorded() {
if (!session.cleanOutput) {
getById("progressContainer").classList.remove("hidden");
}
totalChunksRecorded++;
updateProgressBar();
}
function updateTotalChunksUploaded() {
totalChunksUploaded++;
updateProgressBar();
}
function finishedChunksUploaded() {
try {
getById("progressBar").style.width = "100%";
setTimeout(function () {
if (getById("progressBar").style.width == "100%") {
getById("progressContainer").classList.add("hidden");
}
}, 1000);
} catch (e) {
errorlog(e);
}
}
function updateProgressBar() {
if (totalChunksRecorded > 0) {
var progressPercentage = (totalChunksUploaded / (totalChunksRecorded || 1)) * 100;
getById("progressBar").style.width = progressPercentage + "%";
getById("progressBar").innerHTML = "Upload progress to Dropbox: " + progressPercentage.toFixed(2) + "%";
}
}
async function uploadChunk(chunk, oldCursor = false) {
if (!sessionId) {
errorlog("WHY IS THIS NOT STOPPED?");
return;
}
if (DBXqueue.length && DBXqueue[DBXqueue.length - 1] === false) {
// it's done; ignore anything after this.
console.log("CHUNKS COMPLETED: " + chunkCounter + "/" + (chunkCounter + (DBXqueue.length - 1)));
return;
} else {
DBXqueue.push(chunk);
updateTotalChunksRecorded();
console.log("CHUNKS COMPLETED:" + chunkCounter + "/" + (chunkCounter + DBXqueue.length));
}
console.log(DBXqueue);
if (DBXqueue.length > 1) {
// still uploading
return;
}
if (session.directorUUID) {
var msg = {};
msg.dropbox = DBXqueue.length;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendPeers(msg, msg.UUID);
}
}
if (chunk === false) {
console.log("CHUNKS COMPLETED UPLOADING.. closing dropbox file: " + offset + " " + filename);
//console.log(oldCursor);
console.log({ session_id: sessionId, offset: offset });
session.dbx
.filesUploadSessionFinish({ cursor: { session_id: sessionId, offset: offset }, commit: { path: "/" + filename } })
.then(function (response) {
//console.log(response);
DBXqueue = [];
try {
console.log("File uploaded to Dropbox:", response.result.path_display);
} catch (e) {
errorlog(e);
}
var sessionData = localStorage.getItem("dropboxSession");
if (sessionData) {
// sessionData = [{object1},{object2}]
try {
sessionData = JSON.parse(sessionData);
sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename);
} catch (e) {
errorlog(sessionData);
errorlog(e);
sessionData = [];
}
} else {
sessionData = [];
}
if (sessionData.length) {
localStorage.setItem("dropboxSession", JSON.stringify(sessionData));
} else {
localStorage.removeItem("dropboxSession");
}
sessionId = false;
if (session.directorUUID) {
var msg = {};
msg.dropbox = DBXqueue.length;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendPeers(msg, msg.UUID);
}
}
finishedChunksUploaded();
})
.catch(function (e) {
errorlog(e);
if (e.error_summary) {
if (!session.cleanOutput) {
warnUser("Error\n\n" + error_summary);
}
}
});
} else if (!uploadActive) {
uploadActive = true;
while (DBXqueue.length) {
try {
if (!sessionId) {
errorlog("WHY IS THIS NOT STOPPED ?");
return;
}
let cursor = { session_id: sessionId, offset: offset };
if (DBXqueue[0] === false) {
console.log("CHUNKS COMPLETED UPLOADING... closing dropbox file: " + offset);
await session.dbx
.filesUploadSessionFinish({ cursor: { session_id: sessionId, offset: offset }, commit: { path: "/" + filename } })
.then(function (response) {
//console.log(response);
DBXqueue = [];
try {
console.log("File uploaded to Dropbox:", response.result.path_display);
} catch (e) {
errorlog(e);
}
var sessionData = localStorage.getItem("dropboxSession");
if (sessionData) {
// sessionData = [{object1},{object2}]
try {
sessionData = JSON.parse(sessionData);
sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename);
} catch (e) {
errorlog(sessionData);
errorlog(e);
sessionData = [];
}
} else {
sessionData = [];
}
if (sessionData.length) {
localStorage.setItem("dropboxSession", JSON.stringify(sessionData));
} else {
localStorage.removeItem("dropboxSession");
}
sessionId = false;
if (session.directorUUID) {
var msg = {};
msg.dropbox = DBXqueue.length;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendPeers(msg, msg.UUID);
}
}
finishedChunksUploaded();
})
.catch(function (e) {
errorlog(e);
if (e.error_summary) {
if (!session.cleanOutput) {
warnUser("Error\n\n" + error_summary);
}
}
});
} else {
await session.dbx
.filesUploadSessionAppendV2({ cursor: cursor, close: false, contents: DBXqueue[0] })
.then(function () {
offset += DBXqueue[0].size;
main.vdo.offset = offset;
//localStorage.setItem('dropboxSession', JSON.stringify(main));
var sessionData = localStorage.getItem("dropboxSession");
if (sessionData) {
// sessionData = [{object1},{object2}]
try {
sessionData = JSON.parse(sessionData);
sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename);
sessionData.push(main);
} catch (e) {
errorlog(sessionData);
errorlog(e);
sessionData = [main];
}
} else {
sessionData = [main];
}
localStorage.setItem("dropboxSession", JSON.stringify(sessionData));
DBXqueue.shift(); // we got it uploaded
updateTotalChunksUploaded();
chunkCounter += 1;
console.log("CHUNKs COMPLETED:" + chunkCounter + "/" + (chunkCounter + DBXqueue.length));
if (session.directorUUID) {
var msg = {};
msg.dropbox = DBXqueue.length;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendPeers(msg, msg.UUID);
}
}
})
.catch(function (e) {
errorlog(e);
if (e.error_summary) {
if (!session.cleanOutput) {
warnUser("Error\n\n" + error_summary);
}
}
});
}
} catch (e) {
errorlog(e);
break;
}
}
uploadActive = false;
}
}
return uploadChunk;
})
.catch(function (e) {
errorlog(e);
if (!session.cleanOutput) {
warnUser("Dropbox failed to initialize.\n\nAre you credentials valid? Tokens may expire after a few hours.", 8000);
}
});
}
var recordingBitratePromise = false;
var defaultRecordingBitrate = false;
var lastConfiguredRecordingSetup = false;
async function recordVideo(target, event = null, videoKbps = false) {
// event.currentTarget,this.parentNode.parentNode.dataset.UUID
if (session.record === false) {
warnlog("recordings are disabled by decree of thy host magistrate");
}
if (!target){return;}
var UUID = target.dataset.UUID;
if (!UUID) {
return;
}
var video = session.rpcs[UUID].videoElement;
if (!video) {
return;
}
if (video.stopWriter) {
video.stopWriter();
updateLocalRecordButton(UUID, -1);
return;
} else if (video.startWriter) {
await video.startWriter();
updateLocalRecordButton(UUID, 0);
return;
}
if (event === null) {
if (defaultRecordingBitrate === null) {
updateLocalRecordButton(UUID, -1);
return;
}
} else if (event.ctrlKey || event.metaKey) {
updateLocalRecordButton(UUID, -3);
Callbacks.push([recordVideo, target, null, false]);
log("Record Video queued");
defaultRecordingBitrate = false;
recordingBitratePromise = false;
return;
} else {
defaultRecordingBitrate = false;
recordingBitratePromise = false;
}
log("Record Video Clicked");
if ("recording" in video) {
log("ALREADY RECORDING!");
updateLocalRecordButton(UUID, -2);
video.recorder.stop();
session.requestRateLimit(35, UUID); // 100kbps
if (session.audiobitrate === false) {
session.requestAudioRateLimit(-1, UUID);
}
var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
}
var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
}
var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
}
return;
} else {
updateLocalRecordButton(UUID, 0);
//target.style.backgroundColor = "#FCC";
//target.innerHTML = " Download";
video.recording = true;
}
video.recorder = {};
var configureRecording = {
bitrate: videoKbps,
usePCM: (videoKbps===0 || session.pcm) ? true:false,
audioOnly: (videoKbps!==false && videoKbps<=0) ? true:false
};
if (configureRecording.bitrate === false) {
if (defaultRecordingBitrate == false) {
configureRecording.bitrate = session.recordDefault;
if (session.recordLocal !== false) {
configureRecording.bitrate = session.recordLocal;
} else if (lastConfiguredRecordingSetup!==false){
configureRecording = lastConfiguredRecordingSetup;
}
if (session.pcm){
configureRecording.usePCM = true;
}
if (!recordingBitratePromise) {
window.focus();
recordingBitratePromise = promptRecordingOptions(getTranslation("press-ok-to-record"), false, configureRecording);
}
configureRecording = await recordingBitratePromise;
if (configureRecording === null) {
//target.style.backgroundColor = null;
//target.innerHTML = ' record local';
updateLocalRecordButton(UUID, -1);
target.style.backgroundColor = "";
try {
clearInterval(video.recorder.writer.interval);
} catch (e) {}
delete video.recorder;
delete video.recording;
defaultRecordingBitrate = null;
return;
}
defaultRecordingBitrate = configureRecording;
lastConfiguredRecordingSetup = configureRecording;
} else {
configureRecording = defaultRecordingBitrate;
}
}
if (configureRecording.audioOnly) {
if (session.audiobitrate === false) {
if (configureRecording.usePCM) {
session.requestAudioRateLimit(session.audiobitratePRO || 128, UUID); // PCM
} else {
session.requestAudioRateLimit(configureRecording.bitrate || 32, UUID); // exact? sure. why not.
}
}
} else {
if (configureRecording.bitrate < 50) {
configureRecording.bitrate = 50;
}
session.requestRateLimit(configureRecording.bitrate, UUID); // 3200kbps transfer bitrate. Less than the recording bitrate, to avoid waste.
if (configureRecording.bitrate > 4000) {
if (session.audiobitrate === false) {
if (session.pcm) {
session.requestAudioRateLimit(session.audiobitratePRO, UUID);
} else {
session.requestAudioRateLimit(128, UUID);
}
}
} else if (configureRecording.bitrate > 2500) {
if (session.audiobitrate === false) {
if (session.pcm) {
session.requestAudioRateLimit(session.audiobitratePRO, UUID);
} else {
session.requestAudioRateLimit(80, UUID);
}
}
}
}
//
var cancell = false;
if (typeof video.srcObject === "undefined" || !video.srcObject) {
return;
}
video.recorder.stop = function (restart = false, notify = false) {
if (session.dbx && video.dropbox && video.dropbox[filename]) {
video.dropbox[filename](false);
}
if (video.gdrive && video.gdrive[filename]) {
video.gdrive[filename].addChunk(false);
}
if (!video.recording) {
errorlog("ALREADY STOPPED");
updateLocalRecordButton(UUID, -1);
return;
}
if (notify) {
if (!session.cleanOutput) {
warnUser("A local recording has stopped unexpectedly.");
}
if (session.beepToNotify) {
playtone();
}
target.classList.remove("shake");
setTimeout(
function (target) {
target.classList.add("shake");
},
10,
target
);
}
video.recording = false;
updateLocalRecordButton(UUID, -2);
try {
if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) {
if (video.recorder.mediaRecorder.state !== "inactive" || video.recorder.mediaRecorder.state === "recording") {
video.recorder.mediaRecorder.stop();
}
}
} catch (e) {
errorlog(e);
try {
video.recorder.mediaRecorder.stop();
} catch (e1) {
errorlog(e1);
}
}
session.requestRateLimit(35, UUID); // 100kbps
if (session.audiobitrate === false) {
session.requestAudioRateLimit(-1, UUID);
}
var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
}
var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
}
var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
}
cancell = true;
// log('Recorded Blobs: ', recordedBlobs);
// download();
setTimeout(
(writer1, UUID1, video1) => {
try {
writer1.close();
} catch (e) {}
updateLocalRecordButton(UUID1, -1);
delete video1.recorder;
delete video1.recording;
},
1200,
video.recorder.writer,
UUID,
video
);
};
const { readable, writable } = new TransformStream({
transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b)))
});
var filext = ".webm";
let options = {};
if (!configureRecording.audioOnly) {
var tryCodec = session.recordingVideoCodec || ""; // Simplified condition to assign tryCodec
if (tryCodec && MediaRecorder.isTypeSupported("video/webm;codecs=" + tryCodec)) {
if (!session.cleanOutput) {
console.log("👍 The browser 'says' it supports " + tryCodec);
}
options.mimeType = "video/webm;codecs=" + tryCodec;
if (session.pcm) {
// Fixed the format of the MIME type string
var mimeTypeWithPCM = "video/webm;codecs=" + tryCodec + ",pcm";
if (MediaRecorder.isTypeSupported(mimeTypeWithPCM)) {
options.mimeType = mimeTypeWithPCM;
} else {
options.mimeType = "video/webm;codecs=pcm";
}
}
} else {
// Simplified conditions for PCM support
if (tryCodec) {
warnlog("video/webm;codecs=" + tryCodec + " - is not supported");
}
options.mimeType = session.pcm && MediaRecorder.isTypeSupported("video/webm;codecs=pcm") ? "video/webm;codecs=pcm" : "video/webm";
}
// Simplified bitrate settings
options.videoBitsPerSecond = parseInt(configureRecording.bitrate * 1024);
if (configureRecording.bitrate < 1000) {
options.audioBitsPerSecond = parseInt(100 * 1024);
} else if (configureRecording.bitrate < 6000) {
options.audioBitsPerSecond = parseInt(130 * 1024);
} else if (configureRecording.bitrate < 20000) {
options.audioBitsPerSecond = parseInt(256 * 1024);
} else {
// If configureRecording.bitrate is >= 20000, use bitsPerSecond for total bitrate
options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024);
}
if (iOS && options.mimeType) {
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = "video/mp4";
filext = ".mp4";
}
}
video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options);
//if (session.dbx){
// video.recorder.dropbox = await streamVideoToDropbox(); // i don't want to upload to dropbox remote streams; just local
//}
} else {
options.mimeType = "audio/webm";
if (configureRecording.usePCM) {
if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) {
options.mimeType = "audio/webm;codecs=pcm";
}
} else {
options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024);
}
var stream = createMediaStream();
video.srcObject.getAudioTracks().forEach(track => {
stream.addTrack(track, video.srcObject);
});
if (iOS && options.mimeType) {
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = "video/mp4";
filext = ".mp4";
}
}
video.recorder.mediaRecorder = new MediaRecorder(stream, options);
//if (session.dbx){
// video.recorder.dropbox = await streamVideoToDropbox();
//}
}
var timestamp = Date.now();
var filename = "";
if (session.rpcs[UUID].label && session.rpcs[UUID].streamID) {
filename = session.rpcs[UUID].label || session.rpcs[UUID].streamID;
} else {
filename = session.rpcs[UUID].label + "_" + session.rpcs[UUID].streamID;
}
filename = filename.replace(/[\W]+/g, "_");
filename = filename.substring(0, 200);
filename += "_" + timestamp.toString();
var writer = writable.getWriter();
video.recorder.writer = writer;
readable.pipeTo(streamSaver.createWriteStream(filename.toString() + filext, video.recorder.stop));
pokeIframeAPI("recording-started");
log(options);
function download() {
const blob = new Blob(recordedBlobs, {
type: "video/webm"
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename + filext;
document.body.appendChild(a);
a.click();
setTimeout(
function (uu, aa) {
document.body.removeChild(aa);
window.URL.revokeObjectURL(uu);
},
100,
url,
a
);
}
function handleDataAvailable(event) {
if (event.data && event.data.size > 0) {
//recordedBlobs.push(event.data);
try {
writer.write(event.data); ////////////
if (video.recording) {
updateLocalRecordButton(UUID, parseInt((Date.now() - timestamp) / 1000) || 0);
}
} catch (e) {
warnlog("Stream recording error or ended");
}
try {
if (session.dbx && video.dropbox && video.dropbox[filename]) {
video.dropbox[filename](event.data);
}
if (video.gdrive && video.gdrive[filename]) {
video.gdrive[filename].addChunk(event.data);
}
} catch (e) {
errorlog(e);
}
}
}
video.recorder.mediaRecorder.ondataavailable = handleDataAvailable;
video.recorder.mediaRecorder.onerror = function (event) {
errorlog(event);
console.log("It's possible using &recordcodec=vp8 might resolve recording errors if caused by an incompatible hardware encoder or codec");
video.recorder.stop();
session.requestRateLimit(35, UUID);
if (!session.cleanOutput) {
setTimeout(function () {
warnUser("an error occured with the media recorder; stopping recording");
}, 1);
}
};
video.recorder.mediaRecorder.onstop = function (event) {
log("mediaRecorder stopped");
};
video.srcObject.onended = function (event) {
video.recorder.stop();
//session.requestRateLimit(35, UUID); // changed DEc 05 2023 - not sure this makes sense.
if (!session.cleanOutput) {
setTimeout(function () {
warnUser("stream ended! stopping recording");
}, 1);
}
};
setTimeout(
function (v) {
if (v && v.recorder){
v.recorder.mediaRecorder.start(1000);
}
},
500,
video
); // 100ms chunks
return;
}
function updateGdriveButton(UUID, gdrive, screen = false) {
if (screen) {
var elements = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + UUID + '_screen"]');
} else {
var elements = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + UUID + '"]');
}
if (elements[0]) {
var progressPercentage = parseInt((1000 * gdrive.up) / gdrive.rec) / 10;
elements[0].innerText = progressPercentage + "%";
if (gdrive.state && gdrive.state == 2) {
elements[0].innerText = "GDrive " + elements[0].innerText + " (done)";
} else {
elements[0].innerText += ", " + convertKilobytes(gdrive.rec) + " left";
}
}
}
function updateRemoteRecordButton(UUID, recorder, screen = false) {
if (screen) {
var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '_screen"]');
} else {
var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '"]');
}
if (elements[0]) {
var time = parseInt(recorder) || 0;
if (time == -4) {
if (!session.cleanOutput) {
warnUser("A remote recording has stopped unexpectedly.\n\nDid a user cancel the file downlaod?");
}
if (session.beepToNotify) {
playtone();
}
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].classList.remove("shake");
elements[0].innerHTML = ' stopping...';
setTimeout(
function (ele) {
ele.classList.add("shake");
},
10,
elements[0]
);
} else if (time == -3) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
elements[0].disabled = true;
elements[0].innerHTML = ' Not Supported';
if (!session.cleanOutput) {
setTimeout(function () {
warnUser("The remote browser does not support recording.\n\nPerhaps try local recording instead.");
}, 0);
}
} else if (time == -5) {
if (!session.cleanOutput) {
setTimeout(function () {
warnUser("The remote browser has only experimental support for media recording.\n\nAlso, when this download stops, the remote user may be asked to download the file for it to save.");
}, 0);
}
} else if (time == -2) {
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' stopping...';
} else if (time == -1) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
elements[0].innerHTML = ' Record Remote';
} else {
var minutes = Math.floor(time / 60);
var seconds = time - minutes * 60;
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' ' + minutes + "m : " + zpadTime(seconds) + "s";
}
}
}
function updateLocalRecordButton(UUID, recorder) {
var elements = document.querySelectorAll('[data-action-type="recorder-local"][data--u-u-i-d="' + UUID + '"]');
if (elements[0]) {
var time = parseInt(recorder) || 0;
//target.innerHTML = ' ARMED';
//
if (time == -3) {
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' ARMED';
elements[0].style.backgroundColor = "#BF3F3F";
} else if (time == -2) {
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' stopping...';
elements[0].style.backgroundColor = "";
} else if (time == -1) {
elements[0].classList.remove("pressed");
elements[0].ariaPressed = "false";
elements[0].innerHTML = ' Record Local';
elements[0].style.backgroundColor = "";
} else {
var minutes = Math.floor(time / 60);
var seconds = time - minutes * 60;
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' ' + minutes + "m : " + zpadTime(seconds) + "s";
elements[0].style.backgroundColor = "";
}
}
}
async function recordLocalVideoToggle() {
if (!session.videoElement) {
return;
}
log("recordLocalVideoToggle()");
var ele = getById("recordLocalbutton");
if (ele.dataset.state == "0") {
if (session.videoElement.recorder && session.videoElement.recorder.closing) {
warnlog("already closing");
getById("recordLocalbutton").classList.remove("shake");
getById("recordLocalbutton").classList.add("shake");
setTimeout(function () {
getById("recordLocalbutton").classList.remove("shake");
}, 1000);
return false;
}
ele.dataset.state = "1";
ele.style.backgroundColor = "red";
ele.innerHTML = '';
if ("recording" in session.videoElement) {
errorlog("its already recording ??");
} else {
var res = await recordLocalVideo("start");
log(res);
}
if (session.director) {
var elements = document.querySelectorAll('[data-action-type="recorder-local"][data-sid="' + session.streamID + '"]');
if (elements[0]) {
elements[0].classList.add("pressed");
elements[0].ariaPressed = "true";
elements[0].innerHTML = ' Record';
}
}
return true;
} else {
if ("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));
}
//// PCM 16 SAVING LOGIC
function PCM16(stream) {
if (!stream || !stream.getAudioTracks().length) {
errorlog("no audio track found");
return null;
}
var PCM = stream.getAudioTracks()[0].getSettings();
function audioBufferToWav(buffer, options = {}) {
const numChannels = buffer.numberOfChannels;
const sampleRate = buffer.sampleRate;
const format = options.float32 ? 3 : 1;
const bitDepth = format === 3 ? 32 : 16;
let samples;
if (numChannels === 2) {
samples = interleave(buffer.getChannelData(0), buffer.getChannelData(1));
} else {
samples = buffer.getChannelData(0);
}
return encodeWAV(samples, format, sampleRate, numChannels, bitDepth);
}
function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) {
const bytesPerSample = bitDepth / 8;
const blockAlign = numChannels * bytesPerSample;
const bufferLength = 44 + samples.length * bytesPerSample;
const buffer = new ArrayBuffer(bufferLength);
const dataView = new DataView(buffer);
// referenced from: https://github.com/steveseguin/audiobuffer-to-wav (by Jam3 - MIT lic)
writeString(dataView, 0, "RIFF");
dataView.setUint32(4, 36 + samples.length * bytesPerSample, true);
writeString(dataView, 8, "WAVE");
writeString(dataView, 12, "fmt ");
dataView.setUint32(16, 16, true);
dataView.setUint16(20, format, true);
dataView.setUint16(22, numChannels, true);
dataView.setUint32(24, sampleRate, true);
dataView.setUint32(28, sampleRate * blockAlign, true);
dataView.setUint16(32, blockAlign, true);
dataView.setUint16(34, bitDepth, true);
writeString(dataView, 36, "data");
dataView.setUint32(40, samples.length * bytesPerSample, true);
if (format === 1) {
floatTo16BitPCM(dataView, 44, samples);
} else {
writeFloat32(dataView, 44, samples);
}
return buffer;
}
function interleave(inputL, inputR) {
const length = inputL.length + inputR.length;
const result = new Float32Array(length);
for (let index = 0, inputIndex = 0; index < length; index += 2, inputIndex++) {
result[index] = inputL[inputIndex];
result[index + 1] = inputR[inputIndex];
}
return result;
}
function floatTo16BitPCM(output, offset, input) {
for (let i = 0; i < input.length; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
}
function writeFloat32(output, offset, input) {
for (let i = 0; i < input.length; i++, offset += 4) {
output.setFloat32(offset, input[i], true);
}
}
function writeString(dataView, offset, string) {
for (let i = 0; i < string.length; i++) {
dataView.setUint8(offset + i, string.charCodeAt(i));
}
}
// end reference
PCM.audioContext = new AudioContext({ sampleRate: PCM.sampleRate });
PCM.source = PCM.audioContext.createMediaStreamSource(stream);
PCM.numberOfChannels = PCM.source.channelCount;
PCM.scriptNode = PCM.audioContext.createScriptProcessor(4096, PCM.numberOfChannels, PCM.numberOfChannels); // buffer size, input channels, output channels
PCM.recording = false;
PCM.audioData = [];
for (let i = 0; i < PCM.numberOfChannels; i++) {
PCM.audioData.push([]);
}
PCM.scriptNode.onaudioprocess = audioProcessingEvent => {
if (!PCM.recording) return;
for (let channel = 0; channel < PCM.numberOfChannels; channel++) {
const inputData = audioProcessingEvent.inputBuffer.getChannelData(channel);
PCM.audioData[channel].push(new Float32Array(inputData));
}
};
PCM.source.connect(PCM.scriptNode);
PCM.scriptNode.connect(PCM.audioContext.destination);
PCM.startRecording = function () {
PCM.audioData = [];
for (let i = 0; i < PCM.numberOfChannels; i++) {
PCM.audioData.push([]);
}
PCM.recording = true;
};
PCM.stopRecording = function (filename = "filename") {
PCM.recording = false;
const bufferLength = PCM.audioData[0].length * 4096;
const audioBuffer = PCM.audioContext.createBuffer(PCM.numberOfChannels, bufferLength, PCM.audioContext.sampleRate);
for (let channel = 0; channel < PCM.numberOfChannels; channel++) {
const channelData = audioBuffer.getChannelData(channel);
PCM.audioData[channel].forEach((chunk, index) => {
channelData.set(chunk, index * 4096);
});
}
const wavArrayBuffer = audioBufferToWav(audioBuffer);
const blob = new Blob([wavArrayBuffer], { type: "audio/wav" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename + ".wav";
anchor.click();
URL.revokeObjectURL(url);
};
return PCM;
}
//// END OF PCM 16 SAVING CODE
async function recordLocalVideo(action = null, configureRecording = false, remote = false, altUUID = false) {
// event.currentTarget,this.parentNode.parentNode.dataset.UUID
if (session.record === false) {
warnlog("recordings are disabled by decree of thy host magistrate");
}
log("original",configureRecording);
if (typeof configureRecording !== "object"){
let bitrate = configureRecording!==false ? configureRecording : (session.recordLocal !== false ? session.recordLocal : session.recordDefault);
configureRecording = {
bitrate: bitrate,
usePCM: (bitrate===0 || session.pcm) ? true:false,
audioOnly: (bitrate!==false && bitrate<=0) ? true:false
};
}
if (remote) {
var video = remote;
if (remote.id === "videosource" || remote.id === "screensharesource") {
remote = false;
}
} else if (altUUID) {
var video = session.screenShareElement;
} else {
var video = session.videoElement;
}
if (!video) {
warnlog("video not found");
return;
}
log(video.id);
if ("recording" in video) {
if (action == "estop") {
video.recorder.eStop();
warnlog("EMERGENCY Stopping RECORDING!");
video.recorder.stop();
return;
} else if (action == "stop") {
log("Stopping RECORDING!");
video.recorder.stop();
return;
} else if (action == "start") {
errorlog("ALREADY RECORDING!");
if (remote) {
getById("recordLocalbutton").dataset.state = "1";
getById("recordLocalbutton").style.backgroundColor = "red";
getById("recordLocalbutton").innerHTML = '';
}
return;
} else {
errorlog("STOPPING RECORDING by default toggle!");
video.recorder.stop();
return;
}
return; // this should never happen
} else if (action == "start") {
if (video && video.recorder && video.recorder.closing) {
errorlog("Ingore request. Haven't finished closing the previous recording.");
return;
}
if (video.srcObject && video.srcObject.getTracks && !video.srcObject.getTracks().length){
warnlog("No video or audio tracks to record");
return;
}
if (!MediaRecorder) {
var msg = {};
msg.recorder = -3;
if (altUUID) {
msg.alt = true;
}
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
errorlog("no MediaRecorder");
return;
} else if (SafariVersion || iPad || iOS) {
var msg = {};
msg.recorder = -5;
if (altUUID) {
msg.alt = true;
}
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
log("SAFARI/IOS MODE ENABLED");
}
video.recording = true;
if (remote) {
getById("recordLocalbutton").dataset.state = "1";
getById("recordLocalbutton").style.backgroundColor = "red";
getById("recordLocalbutton").innerHTML = '';
}
} else if (action == "stop") {
warnlog("stop not sensible");
return;
} else {
log("action is :" + action);
if (video && video.recorder && video.recorder.closing) {
errorlog("Ingore request. Haven't finished closing the previous recording.");
return;
}
if (!remote) {
getById("recordLocalbutton").dataset.state = "1";
getById("recordLocalbutton").style.backgroundColor = "red";
getById("recordLocalbutton").innerHTML = '';
}
video.recording = true;
}
video.recorder = {};
if (!configureRecording.audioOnly && configureRecording.bitrate<50){
configureRecording.bitrate = 50;
}
if (typeof video.srcObject === "undefined" || !video.srcObject) {
errorlog("video.srcObject undefined");
return;
}
log(configureRecording);
var timestamp = Date.now();
var filename = "";
if (session.label || session.streamID) {
filename = session.label || session.streamID;
filename = filename.replace(/[\W]+/g, "_");
filename = filename.substring(0, 200);
}
filename += "_" + timestamp.toString();
log("filename: " + filename);
video.recorder.eStop = function () {
try {
video.recorder.writer.close();
clearInterval(video.recorder.writer.interval);
} catch (e) {}
};
video.recorder.stop = function (restart = false, notify = false) {
if (session.dbx && video.dropbox && video.dropbox[filename]) {
video.dropbox[filename](false);
}
if (video.gdrive && video.gdrive[filename]) {
video.gdrive[filename].addChunk(false);
}
try {
if (!remote) {
if (restart) {
if (getById("recordLocalbutton").dataset.state == 2) {
getById("recordLocalbutton").dataset.state = "0";
getById("recordLocalbutton").style.backgroundColor = "";
getById("recordLocalbutton").innerHTML = '';
if (restart !== true) {
warnUser("Media Recording Stopped due to an error: " + restart);
} else {
warnUser("Media Recording Stopped due to an error.");
}
restart = false;
} else {
getById("recordLocalbutton").innerHTML = '';
getById("recordLocalbutton").dataset.state = "2";
}
} else {
getById("recordLocalbutton").dataset.state = "0";
getById("recordLocalbutton").style.backgroundColor = "";
getById("recordLocalbutton").innerHTML = '';
if (notify) {
if (!session.cleanOutput) {
warnUser("A recording has stopped unexpectedly.");
}
if (session.beepToNotify) {
playtone();
}
getById("recordLocalbutton").classList.remove("shake");
setTimeout(function () {
getById("recordLocalbutton").classList.add("shake");
}, 10);
}
}
}
} catch (e) {
errorlog(e);
}
if (!video.recording) {
errorlog("ALREADY STOPPED");
return;
}
if (!video.recorder || video.recorder.closing) {
errorlog("it's still closing; can't start until its done");
return;
}
video.recorder.closing = true; // start the closing process
try {
if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) {
if (video.recorder.mediaRecorder.state !== "inactive") {
video.recorder.mediaRecorder.stop();
}
}
} catch (e) {
errorlog(e);
try {
video.recorder.mediaRecorder.stop();
} catch (e1) {
errorlog(e1);
}
}
// video.recording = false;
setTimeout(
(configureRecording, altUUID, video) => {
try {
video.recorder.writer.close();
} catch (e) {
errorlog(e);
}
try {
clearInterval(video.recorder.writer.interval);
} catch (e) {
errorlog(e);
}
pokeIframeAPI("recording-stopped");
if (!remote) {
try {
if (session.directorUUID) {
var msg = {};
msg.recorder = -1;
if (altUUID) {
msg.alt = true;
}
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
}
} catch (e) {
errorlog(e);
}
}
try {
if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) {
if (video.recorder.mediaRecorder.state !== "inactive") {
video.recorder.mediaRecorder.stop();
}
}
} catch (e) {
errorlog(e);
try {
video.recorder.mediaRecorder.stop();
} catch (e1) {
errorlog(e1);
}
}
try {
delete video.recorder;
delete video.recording;
} catch (e) {}
if (!remote) {
if (restart) {
setTimeout(
function (configureRecording, altUUID) {
recordLocalVideo("start", configureRecording, false, altUUID);
},
0,
configureRecording,
altUUID
);
}
}
},
500,
configureRecording,
altUUID,
video
);
if (!remote) {
try {
if (session.directorUUID) {
var msg = {};
if (notify) {
msg.recorder = -4; // user aborted
} else {
msg.recorder = -2;
}
if (altUUID) {
msg.alt = true;
}
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
}
} catch (e) {
errorlog(e);
}
}
};
let options = {};
let filext = ".webm";
log("setting up options");
if (!configureRecording.audioOnly) {
log("videoKbps: " + configureRecording.bitrate);
var tryCodec = session.recordingVideoCodec || ""; // Simplified condition to assign tryCodec
if (tryCodec && MediaRecorder.isTypeSupported("video/webm;codecs=" + tryCodec)) {
if (!session.cleanOutput) {
console.log("👍 The browser 'says' it supports " + tryCodec);
}
options.mimeType = "video/webm;codecs=" + tryCodec;
if (configureRecording.usePCM) {
// Fixed the format of the MIME type string
var mimeTypeWithPCM = "video/x-matroska;codecs=" + tryCodec + ",pcm";
if (MediaRecorder.isTypeSupported(mimeTypeWithPCM)) {
options.mimeType = mimeTypeWithPCM;
} else {
options.mimeType = "video/webm;codecs=pcm";
}
}
} else {
// Simplified conditions for PCM support
if (tryCodec) {
warnlog("video/webm;codecs=" + tryCodec + " - is not supported");
}
options.mimeType = configureRecording.usePCM && MediaRecorder.isTypeSupported("video/webm;codecs=pcm") ? "video/webm;codecs=pcm" : "video/webm";
}
// Simplified bitrate settings
options.videoBitsPerSecond = parseInt(configureRecording.bitrate * 1024);
if (configureRecording.bitrate < 1000) {
options.audioBitsPerSecond = parseInt(100 * 1024);
} else if (configureRecording.bitrate < 6000) {
options.audioBitsPerSecond = parseInt(130 * 1024);
} else if (configureRecording.bitrate < 20000) {
options.audioBitsPerSecond = parseInt(256 * 1024);
} else {
// If configureRecording.bitrate is >= 20000, use bitsPerSecond for total bitrate
options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024);
}
if (iOS && options.mimeType) {
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = "video/mp4";
filext = ".mp4";
}
}
video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options);
try {
log(options);
video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options);
} catch (e) {
warnlog(e);
try {
errorlog("options failed");
video.recorder.mediaRecorder = new MediaRecorder(video.srcObject);
} catch (e) {
errorlog(e);
errorlog("Failing the recording");
var msg = {};
msg.recorder = -3;
if (altUUID) {
msg.alt = true;
}
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
getById("recordLocalbutton").dataset.state = "0";
getById("recordLocalbutton").style.backgroundColor = "";
getById("recordLocalbutton").innerHTML = '';
return;
}
}
if (session.dbx) {
if (!video.dropbox) {
video.dropbox = {};
}
video.dropbox[filename] = await streamVideoToDropbox(filename.toString() + filext); // i don't want to upload to dropbox remote streams; just local
if (!video.dropbox[filename]) {
delete video.dropbox[filename];
}
}
if (session.gdrive) {
if (!video.gdrive) {
video.gdrive = {};
}
if (session.gdrive === true) {
video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext);
} else if (session.gdrive.sessionUri) {
video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext, session.gdrive.sessionUri); // filename isn't actually being used here
session.gdrive = false;
} else {
errorlog("gdrive partially setup?");
video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext);
}
if (!video.gdrive[filename]) {
delete video.gdrive[filename];
}
}
log(video.recorder.mediaRecorder);
} else {
log("Audio only?");
options.mimeType = "audio/webm";
if (configureRecording.usePCM) {
if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) {
options.mimeType = "audio/webm;codecs=pcm";
}
} else {
options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024);
}
var stream = createMediaStream();
var audioTrack = false;
video.srcObject.getAudioTracks().forEach(track => {
audioTrack = true;
stream.addTrack(track, video.srcObject);
});
if (!audioTrack) {
errorlog("Failing the recording; no audio track");
try {
video.recorder.writer.close();
} catch (e) {}
try {
clearInterval(video.recorder.writer.interval);
} catch (e) {}
try {
delete video.recorder;
delete video.recording;
} catch (e) {}
var msg = {};
msg.recorder = -3;
if (altUUID) {
msg.alt = true;
}
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
getById("recordLocalbutton").dataset.state = "0";
getById("recordLocalbutton").style.backgroundColor = "";
getById("recordLocalbutton").innerHTML = '';
return;
} else {
if (iOS && options.mimeType) {
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = "video/mp4";
}
}
try {
video.recorder.mediaRecorder = new MediaRecorder(stream, options);
} catch (e) {
warnlog(e);
try {
errorlog("options failed. failing safe..");
video.recorder.mediaRecorder = new MediaRecorder(stream);
} catch (e) {
errorlog(e);
errorlog("Fail safe failed; closing the recording");
try {
video.recorder.writer.close();
} catch (e) {}
try {
clearInterval(video.recorder.writer.interval);
} catch (e) {}
try {
delete video.recorder;
delete video.recording;
} catch (e) {}
var msg = {};
msg.recorder = -3;
if (altUUID) {
msg.alt = true;
}
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
getById("recordLocalbutton").dataset.state = "0";
getById("recordLocalbutton").style.backgroundColor = "";
getById("recordLocalbutton").innerHTML = '';
return;
}
}
if (session.dbx) {
if (!video.dropbox) {
video.dropbox = {};
}
video.dropbox[filename] = await streamVideoToDropbox(filename.toString() + filext); // i don't want to upload to dropbox remote streams; just local
if (!video.dropbox[filename]) {
delete video.dropbox[filename];
}
}
if (session.gdrive) {
if (!video.gdrive) {
video.gdrive = {};
}
if (session.gdrive === true) {
video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext);
} else if (session.gdrive.sessionUri) {
video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext, session.gdrive.sessionUri); // filename isn't actually being used here
session.gdrive = false;
} else {
video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext);
errorlog("Gdrive only partially setup");
}
if (!video.gdrive[filename]) {
delete video.gdrive[filename];
}
}
}
}
log(options);
function createLock() {
let isLocked = false;
let queue = Promise.resolve();
return {
acquire: async () => {
const release = () => { isLocked = false; };
while (isLocked) {
await new Promise(resolve => setTimeout(resolve, 10));
}
isLocked = true;
return release;
}
};
}
var chunkQueue = [];
async function handleDataAvailable(event, process = true) {
if (!video.recorder.writerLock) {
video.recorder.writerLock = createLock();
}
// Handle existing queue first
while (chunkQueue.length && process) {
const ret = await handleDataAvailable(chunkQueue.shift(), false);
if (ret === false) {
return;
}
}
if (event.data && event.data.size > 0) {
try {
const release = await video.recorder.writerLock.acquire();
try {
if (video && video.recorder && video.recorder.writer && video.recorder.writer._ownerWritableStream && video.recorder.writer._ownerWritableStream._state === "writable"){
await video.recorder.writer.write(event.data);
} else {
throw new Error("Writer not open");
}
} catch (e) {
if (process === true) {
chunkQueue.push(event);
} else {
chunkQueue.unshift(event);
release();
return false;
}
} finally {
release();
}
// Rest of the existing code for messaging and cloud uploads
if (session.directorList.length) {
if (video.recording) {
var msg = {};
if (altUUID) {
msg.alt = true;
}
msg.recorder = parseInt((Date.now() - timestamp) / 1000) || 0;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
}
}
if (session.dbx && video.dropbox && video.dropbox[filename]) {
video.dropbox[filename](event.data);
}
if (video.gdrive && video.gdrive[filename]) {
video.gdrive[filename].addChunk(event.data);
}
} catch (e) {
errorlog(e);
}
}
}
video.recorder.mediaRecorder.ondataavailable = (event) => {
handleDataAvailable(event).catch(e => errorlog(e));
};
video.recorder.mediaRecorder.onerror = function (event) {
errorlog(event);
console.log("It's possible using &recordcodec=vp8 might resolve recording errors if caused by an incompatible hardware encoder or codec");
if (event && event.error && event.error.name) {
video.recorder.stop(event.error.name);
} else {
video.recorder.stop(true);
}
};
video.srcObject.onended = function (event) {
video.recorder.stop();
};
try {
video.recorder.iteration = 0;
video.recorder.setupWriter = async function setupWriter(video) {
if (video.recorder.writer) {
try {
video.recorder.writer.close();
await sleep(1000);
// we don't cancel the interval obviously
} catch (e) {
errorlog(e);
}
}
var { readable, writable } = new TransformStream({
transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b)))
});
var writer = await writable.getWriter();
var addon = "";
if (video.recorder.iteration != 0) {
addon = "_" + video.recorder.iteration;
}
video.recorder.iteration += 1;
readable.pipeTo(streamSaver.createWriteStream(filename.toString() + filext + addon, video.recorder.stop));
video.recorder.writer = writer;
};
await video.recorder.setupWriter(video);
if (session.recordingInterval) {
// minutes
function intervalClosure(video) {
var intervalId = setInterval(
function (video) {
try {
video.recorder.setupWriter(video);
} catch (e) {
clearInterval(intervalId);
}
},
1000 * 60 * session.recordingInterval,
video
);
video.recorder.writer.interval = intervalId;
}
intervalClosure(video);
}
video.recorder.mediaRecorder.start(1000); // 100ms chunks
log("started recording");
pokeIframeAPI("recording-started");
getById("recordLocalbutton").dataset.state = "1";
getById("recordLocalbutton").style.backgroundColor = "red";
getById("recordLocalbutton").innerHTML = '';
if (session.directorList.length) {
var msg = {};
if (altUUID) {
msg.alt = true;
}
msg.recorder = 0;
for (var i = 0; i < session.directorList.length; i++) {
msg.UUID = session.directorList[i];
session.sendMessage(msg, msg.UUID);
}
}
} catch (e) {
errorlog(e);
}
return;
}
function localGlobalRecordStart() {
document.querySelectorAll("[data-action-type='recorder-local']").forEach(target => {
var UUID = target.dataset.UUID;
if (!UUID) {
return;
}
var video = session.rpcs[UUID].videoElement;
if (!video) {
return;
}
if (!video.stopWriter) {
recordVideo(target); // if not started, start
}
});
recordLocalVideo("start"); // self
}
function localGlobalRecordStop() {
document.querySelectorAll("[data-action-type='recorder-local']").forEach(target => {
var UUID = target.dataset.UUID;
if (!UUID) {
return;
}
var video = session.rpcs[UUID].videoElement;
if (!video) {
return;
}
if (video.stopWriter) {
recordVideo(target); // if started, stop
}
});
recordLocalVideo("stop"); // self
}
async function remoteGlobalRecordStart() {
window.focus();
var bitrate = await promptAlt(miscTranslations["what-bitrate"], false, false, 6000);
document.querySelectorAll("[data-action-type='recorder-remote']").forEach(target => {
requestVideoRecord(target, true, bitrate);
});
}
function remoteGlobalRecordStop() {
document.querySelectorAll("[data-action-type='recorder-remote']").forEach(target => {
if (target.classList.contains("pressed")) {
requestVideoRecord(target, false);
}
});
}
session.onTrack = function (event, UUID) {
if (session.badStreamList.includes(session.rpcs[UUID].streamID)) {
errorlog("new connection is contained in badStreamList 2! This shouldn't happen");
// we will have none of this.
return;
}
var newTracks = [];
var newStream = false;
if (event.streams && event.streams[0]) {
newStream = event.streams[0];
newTracks = newStream.getTracks();
} else if (event.track) {
newTracks.push(event.track);
} else {
errorlog("Something went wrong with incoming track..");
return;
}
if (session.rpcs[UUID].streamSrc) {
var tracks = session.rpcs[UUID].streamSrc.getTracks();
for (var i = 0; i < newTracks.length; i++) {
for (var j = 0; j < tracks.length; j++) {
if (newTracks[i].id == tracks[j].id && newTracks[i].kind == tracks[j].kind) {
newTracks.splice(i, 1);
i--;
break;
}
}
}
}
var screenshare = false;
if (session.rpcs[UUID].screenIndexes && session.rpcs[UUID].getReceivers && session.rpcs[UUID].screenIndexes.length) {
log("session.rpcs[UUID].screenIndexes: " + session.rpcs[UUID].screenIndexes);
var receievers = session.rpcs[UUID].getReceivers(); // excluded
for (var i = 0; i < receievers.length; i++) {
for (var j = 0; j < newTracks.length; j++) {
if (receievers[i].track && receievers[i].track.id == newTracks[j].id && receievers[i].track.kind == newTracks[j].kind) {
for (var k = 0; k < session.rpcs[UUID].screenIndexes.length; k++) {
if (session.rpcs[UUID].screenIndexes[k] == i) {
screenshare = true;
break;
}
}
}
if (screenshare) {
break;
}
}
if (screenshare) {
break;
}
}
}
log("screenshare: " + screenshare);
log(session.rpcs[UUID].streamID);
try {
var index = newTracks.length;
while (index--) {
if (newTracks[index].kind == "video") {
if (session.novideo !== false && !session.novideo.includes(session.rpcs[UUID].streamID)) {
if (!(screenshare && session.novideo.includes(session.rpcs[UUID].streamID+":s"))){
newTracks.splice(index, 1);
}
continue;
} else if (session.rpcs[UUID].settings && session.rpcs[UUID].settings.allowscreenvideo && screenshare) {
//newTracks.splice(index,1);
continue;
} else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.video) {
newTracks.splice(index, 1);
continue;
}
} else if (newTracks[index].kind == "audio") {
if (session.noaudio !== false && !session.noaudio.includes(session.rpcs[UUID].streamID)) {
if (!(screenshare && session.noaudio.includes(session.rpcs[UUID].streamID+":s"))){
newTracks.splice(index, 1);
}
continue;
} else if (session.excludeaudio && session.excludeaudio.includes(session.rpcs[UUID].streamID)) {
newTracks.splice(index, 1);
continue;
} else if (session.rpcs[UUID].settings && session.rpcs[UUID].settings.allowscreenaudio && screenshare) {
//newTracks.splice(index,1);
continue;
} else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.audio) {
newTracks.splice(index, 1);
continue;
}
}
}
} catch (e) {
errorlog(e);
}
if (!newTracks.length) {
warnlog("NO NEW TRACKS?");
return;
}
if (session.encodedInsertableStreams && session.rpcs[UUID] && session.rpcs[UUID].getReceivers) {
var receievers = session.rpcs[UUID].getReceivers(); // excluded
for (var i = 0; i < receievers.length; i++) {
for (var j = 0; j < newTracks.length; j++) {
if (receievers[i].track && receievers[i].track.id == newTracks[j].id && receievers[i].track.kind == newTracks[j].kind) {
try {
setupReceiverTransform(receievers[i]);
} catch (e) {
errorlog(e);
}
}
}
}
}
if (screenshare) {
session.setupScreenShareAddon(newTracks, UUID);
return;
}
//if (session.buffer!==false){
playoutdelay(UUID);
//}
session.directorSpeakerMute(); // apply any mute states to new tracks.
session.directorDisplayMute();
if (newStream) {
newStream.onremovetrack = function (e1) {
try {
warnlog("Track was removed");
session.rpcs[UUID].streamSrc.getTracks().forEach(trk => {
if (trk.id == e1.track.id && trk.kind == e1.track.kind) {
session.rpcs[UUID].streamSrc.removeTrack(trk);
}
});
if (e1.track.kind == "video") {
updateIncomingVideoElement(UUID, true, false);
} else {
updateIncomingVideoElement(UUID, false, true);
}
// updateIncomingVideoElement(UUID); // session.rpcs[UUID].videoElement.srcObject = session.rpcs[UUID].streamSrc;
setTimeout(function () {
updateMixer();
}, 1);
} catch (e) {}
};
newStream.onerror = function (e1) {
errorlog(e1);
try {
warnlog("Track threw an error; going to reconnect it");
session.rpcs[UUID].streamSrc.getTracks().forEach(trk => {
try {
if (trk.id == e1.track.id && trk.kind == e1.track.kind) {
session.rpcs[UUID].streamSrc.removeTrack(trk);
}
} catch (e) {}
});
if (e1.track.kind == "video") {
updateIncomingVideoElement(UUID, true, false);
} else {
updateIncomingVideoElement(UUID, false, true);
}
setTimeout(function () {
updateMixer();
}, 1);
} catch (e) {
errorlog(e);
}
};
}
createRichVideoElement(UUID);
if (!session.rpcs[UUID].streamSrc) {
session.rpcs[UUID].streamSrc = createMediaStream();
mediaSourceUpdated(UUID, session.rpcs[UUID].streamID);
}
var videoAdded = false;
var audioAdded = false;
newTracks.forEach(trk => {
if (trk.kind == "video") {
videoAdded = true;
} else if (trk.kind == "audio") {
audioAdded = true;
}
log("adding track");
session.rpcs[UUID].streamSrc.addTrack(trk);
});
if (newTracks.length > session.rpcs[UUID].streamSrc.getTracks().length) {
errorlog("Not all the tracks were added to the local stream; are the tracks' IDs not unique?");
console.log("streamSrc total tracks: " + session.rpcs[UUID].streamSrc.getTracks().length);
}
if (isIFrame && session.sendframes) {
sendFrameHandler(newTracks, UUID);
}
if (audioAdded && videoAdded) {
updateIncomingVideoElement(UUID);
} else if (videoAdded) {
updateIncomingVideoElement(UUID, true, false);
} else if (audioAdded) {
updateIncomingVideoElement(UUID, false, true);
if (!session.roomid && session.view && !session.permaid) {
setTimeout(function () {
updateMixer();
}, 10); // video already has an auto-start, with aspect ratio size change. audio doesn't.
}
}
if (session.twilio && audioAdded) {
session.twilio.updateMixer(UUID);
}
return session;
};
function sendFrameHandler(tracks, UUID=null){
tracks.forEach(async trk => {
if (trk.kind !== "video") return;
log("STARTING NEW SEND STREAM VIDEO TRACK");
const startImageStream = async () => {
log("startImageStream");
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", { willReadFrequently: true });
const processor = new MediaStreamTrackProcessor(trk);
const reader = processor.readable.getReader();
while (true) {
try {
const {done, value: frame} = await reader.read();
if (done) break;
try {
canvas.width = frame.displayWidth;
canvas.height = frame.displayHeight;
ctx.drawImage(frame, 0, 0);
const format = typeof session.sendframes === "string" ? session.sendframes : "webp";
const imageData = canvas.toDataURL(`image/${format}`, 0.8);
parent.postMessage({
type: 'frame',
frame: imageData,
UUID,
streamID: (session.rpcs[UUID] ? session.rpcs[UUID].streamID : null),
trackID: trk.id,
kind: trk.kind,
format: format
}, session.sendframes);
} finally {
frame.close();
}
} catch (e) {
console.error("Error processing image frame:", e);
break;
}
}
};
const startFrameStream = async () => {
log("startFrameStream");
const processor = new MediaStreamTrackProcessor(trk);
const reader = processor.readable.getReader();
while (true) {
try {
const {done, value} = await reader.read();
if (done) {
if (value) value.close();
break;
}
try {
parent.postMessage({
frame: value,
UUID,
streamID: (session.rpcs[UUID] ? session.rpcs[UUID].streamID : null),
trackID: trk.id,
kind: trk.kind,
type: "frame"
}, session.sendframes, [value]);
} finally {
value.close();
}
} catch (e) {
console.error("Error processing video frame:", e);
if (e.name === "DataCloneError") {
// Fall back to image stream if frame transfer fails
return startImageStream();
}
break;
}
}
};
try {
if (typeof MediaStreamTrackProcessor === 'function') {
try {
new SharedArrayBuffer(1);
await startFrameStream();
} catch(e){
console.warn(e);
await startImageStream();
}
} else {
await startImageStream();
}
} catch (e) {
console.error("Stream processing failed:", e);
}
});
}
function updateIncomingVideoElement(UUID, video = true, audio = true) {
if (!session.rpcs[UUID].videoElement) {
return;
}
if (!session.rpcs[UUID].streamSrc) {
return;
}
if (!session.rpcs[UUID].videoElement.srcObject) {
session.rpcs[UUID].videoElement.srcObject = createMediaStream();
}
if (video) {
var tracks = session.rpcs[UUID].videoElement.srcObject.getVideoTracks(); // add video track
session.rpcs[UUID].streamSrc.getVideoTracks().forEach(trk => {
var added = false;
tracks.forEach(trk2 => {
if (trk.id == trk2.id && trk.kind == trk2.kind) {
added = true;
}
});
if (!added) {
session.rpcs[UUID].videoElement.srcObject.getVideoTracks().forEach(trk2 => {
// make sure only one video track is added at a time.
session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2);
});
if (trk.muted && trk.kind == "video" && session.director) {
trk.onunmute = function (e) {
if (!session.rpcs[UUID]) {
return;
}
this.onunmute = null;
warnlog("ON UN-MUTE");
updateIncomingVideoElement(UUID, true, false);
};
} else {
if (session.rpcs[UUID].videoElement.controls) {
session.rpcs[UUID].videoElement.controls = session.showControls || false;
if (session.showControls === null) {
setTimeout(
function (ele) {
if (ele) {
ele.controls = true;
}
},
500,
session.rpcs[UUID].videoElement
);
}
}
session.rpcs[UUID].videoElement.srcObject.addTrack(trk);
mediaVideoTrackUpdated(UUID, session.rpcs[UUID].streamID);
}
}
});
if ((session.motionSwitch || session.motionRecord) && !session.rpcs[UUID].motionDetectionInterval) {
session.rpcs[UUID].motionDetectionInterval = setTimeout(function () {
setInterval(function () {
motionDetection(session.rpcs[UUID].videoElement, session.motionSwitch || session.motionRecord);
}, 400);
}, 2000);
}
}
if (audio) {
updateIncomingAudioElement(UUID); // do the same for audio now.
if (!video) {
if (session.rpcs[UUID] && session.rpcs[UUID].videoElement) {
// this bit of code fixes an issue where the volume button doesn't show, after adding an audio track for the first time
var pastMuteState = session.rpcs[UUID].videoElement.muted;
session.rpcs[UUID].videoElement.muted = !pastMuteState;
session.rpcs[UUID].videoElement.muted = pastMuteState;
}
}
}
if ((session.optimize===0) && session.activatedStreams.size && session.rpcs[UUID] && session.rpcs[UUID].streamID){
if (session.activatedStreams.has(session.rpcs[UUID].streamID)){
if (session.activatedStreamsQueue[session.rpcs[UUID].streamID]){
let msgs = session.activatedStreamsQueue[session.rpcs[UUID].streamID];
delete session.activatedStreamsQueue[session.rpcs[UUID].streamID];
msgs.forEach(msgWithTime=>{
log(msgWithTime);
if (msgWithTime.time && (msgWithTime.time > Date.now() - 10000)){
session.directorActions(msgWithTime.msg);
}
});
}
}
}
}
function updateIncomingAudioElement(UUID) {
// this can be called when turning on/off inbound audio processing.
if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].streamSrc) {
return;
}
if (!session.rpcs[UUID].videoElement.srcObject) {
session.rpcs[UUID].videoElement.srcObject = createMediaStream();
}
log("updateIncomingAudioElement: " + UUID);
if (session.audioEffects === true || session.pushLoudness || (session.rpcs[UUID].isolatedChannel !== undefined)) {
var tracks = session.rpcs[UUID].streamSrc.getAudioTracks();
if (tracks.length) {
var track = tracks[0];
track = addAudioPipeline(UUID, track);
log(track);
var added = false;
var tracks2 = session.rpcs[UUID].videoElement.srcObject.getAudioTracks();
log(tracks2);
tracks2.forEach(trk2 => {
if (trk2.label && trk2.label == "MediaStreamAudioDestinationNode") {
// an old morphed node; delete it.
session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2);
} else if (track.id == trk2.id && track.kind == trk2.kind) {
// maybe it didn't morph; already added either way
added = true;
} else if (tracks[0].id == trk2.id && tracks[0].kind == trk2.kind && track.id != tracks[0].id) {
// remove original audio track that is now morphed
session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2);
}
});
if (!added) {
session.rpcs[UUID].videoElement.srcObject.addTrack(track);
mediaAudioTrackUpdated(UUID, session.rpcs[UUID].streamID);
}
} else {
session.rpcs[UUID].videoElement.srcObject.getAudioTracks().forEach(trk => {
// make sure to remove all tracks.
session.rpcs[UUID].videoElement.srcObject.remove(trk);
});
}
} else {
var expected = [];
tracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); // add audio tracks
session.rpcs[UUID].streamSrc.getAudioTracks().forEach(trk => {
var added = false;
tracks.forEach(trk2 => {
if (trk.id == trk2.id && trk.kind == trk2.kind) {
added = true;
expected.push(trk2); //
}
});
if (!added) {
session.rpcs[UUID].videoElement.srcObject.addTrack(trk);
mediaAudioTrackUpdated(UUID, session.rpcs[UUID].streamID);
}
});
tracks.forEach(trk => {
var added = false;
expected.forEach(trk2 => {
if (trk.id == trk2.id && trk.kind == trk2.kind) {
added = true;
}
});
if (!added) {
// not expected. so lets delete.
warnlog("this shouldn't happen that often, audio track orphaned. removing it");
session.rpcs[UUID].videoElement.srcObject.removeTrack(trk);
}
});
}
if (session.mixMinus) {
stream = mixMinusAudio(UUID); // only works with p2p; no chunked mode.
}
}
function cycleStyleOptions() {
session.style += 1;
if (session.style > 6) {
session.style = 1;
} else if (session.style == 4) {
session.style = 5;
}
for (var UUID in session.rpcs) {
if (session.rpcs[UUID].canvas) {
try {
if (session.rpcs[UUID].canvas) {
session.rpcs[UUID].canvas.remove();
}
} catch (e) {}
session.rpcs[UUID].canvas = null;
}
updateIncomingAudioElement(UUID);
}
updateMixer();
}
function addAudioPipeline(UUID, track) {
// INBOUND AUDIO EFFECTS ; audio tracks only
try {
if (session.disableViewerWebAudioPipeline) {
log("ignoring addAudioPipeline - disableViewerWebAudioPipeline is enabled (noap)");
return track;
}
log("Triggered webaudio effects path");
for (var tid in session.rpcs[UUID].inboundAudioPipeline) {
delete session.rpcs[UUID].inboundAudioPipeline[tid]; // get rid of old nodes.
}
var trackid = track.id; // this is an audio track, or should be.
session.rpcs[UUID].inboundAudioPipeline[trackid] = {};
session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream = createMediaStream();
session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream.addTrack(track);
if (ChromiumVersion && session.audioEffects) {
// I'm going to deprecate this.
session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio = createAudioElement(); // TODO: I don't know if this mutedAudio thing matters any more, in recent versions of Chrome, since it won't play even if muted.
session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = true;
session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.playsinline = true; // ## Added Oct 9th 2022. Not sure it's does anything, but might help with iPhones?
session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.srcObject = session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream; // needs to be added as an streamed element to be usable, even if its hidden
session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = true;
//session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.volume = 0.01;
session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio
.play()
.then(_ => {
//session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = false;
log("playing 1");
})
.catch(warnlog);
}
// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaStreamTrackSource
var source = session.audioCtx.createMediaStreamSource(session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream);
//////////////////
var screwedUp = false;
session.rpcs[UUID].inboundAudioPipeline[trackid].destination = false;
if (session.rpcs[UUID].isolatedChannel !== undefined) {
log("Isolating channel: " + session.rpcs[UUID].isolatedChannel);
session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
source = isolateChannel(source, session.rpcs[UUID].isolatedChannel);
screwedUp = true;
}
if (session.sync !== false) {
log("adding a delay node to audio");
source = addDelayNode(source, UUID, trackid);
screwedUp = true;
}
if (session.style === 2) {
log("adding a fftwave node to audio");
try {
if (session.rpcs[UUID].inboundAudioPipeline[trackid]) {
// clear audioMeterGuest, if active.
clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
}
} catch (e) {}
source = fftWaveform(source, UUID, trackid);
} else if (session.style === 3 || session.meterStyle) {
log("adding a loudness meter node to audio");
source = audioMeterGuest(source, UUID, trackid);
} else if (session.audioMeterGuest) {
log("adding a loudness meter node to audio");
source = audioMeterGuest(source, UUID, trackid);
} else if (session.activeSpeaker) {
log("adding a loudness meter node to audio");
source = audioMeterGuest(source, UUID, trackid);
} else if (session.quietOthers) {
log("adding a loudness meter node to audio");
source = audioMeterGuest(source, UUID, trackid);
} else if (session.pushLoudness) {
source = audioMeterGuest(source, UUID, trackid);
} else {
try {
if (session.rpcs[UUID].inboundAudioPipeline[trackid]) {
// nothign active, so clear
clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
}
} catch (e) {}
}
if (session.playChannel) {
session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
source = selectChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.playChannel);
screwedUp = true;
} else if (session.rpcs[UUID].channelOffset !== false) {
log("custom offset set");
session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
source = offsetChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.rpcs[UUID].channelOffset, session.rpcs[UUID].channelWidth);
screwedUp = true;
} else if (session.offsetChannel !== false) {
// proably better to do this last.
log("adding offset channels");
session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
source = offsetChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.offsetChannel, session.channelWidth);
screwedUp = true;
} else if (session.panning !== false) {
// proably better to do this last.
log("adding offset channels");
session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
source = stereoPanning(source, UUID, trackid, session.panning);
screwedUp = true;
} else if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.manualSink) {
screwedUp = true; // added June-3-22 to allow for custom outputs to different audio output destinations.
}
if (screwedUp) {
warnlog("screwedUp mode activated. dun dun");
if (session.rpcs[UUID].inboundAudioPipeline[trackid].destination === false) {
session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination();
}
source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].destination);
try {
if (session.firstPlayTriggered && session.audioCtx.state == "suspended") {
log("trying to resume..");
session.audioCtx.resume();
}
} catch (e) {
warnlog("session.audioCtx.resume(); failed");
}
return session.rpcs[UUID].inboundAudioPipeline[trackid].destination.stream.getAudioTracks()[0];
}
try {
if (session.firstPlayTriggered && session.audioCtx.state == "suspended") {
session.audioCtx.resume();
}
} catch (e) {
warnlog("session.audioCtx.resume(); failed 2");
}
return track;
} catch (e) {
errorlog(e);
}
return track;
}
function processMiniInfoUpdate(miniInfo, UUID) {
if ("qlr" in miniInfo) {
session.rpcs[UUID].stats.info.quality_limitation_reason = miniInfo.qlr;
}
if ("con" in miniInfo) {
session.rpcs[UUID].stats.info.conn_type = miniInfo.con;
}
if ("cpu" in miniInfo) {
session.rpcs[UUID].stats.info.cpuLimited = miniInfo.cpu;
if (session.rpcs[UUID].signalMeter) {
if (miniInfo.cpu) {
session.rpcs[UUID].signalMeter.dataset.cpu = "1";
} else if ("cpu" in miniInfo) {
session.rpcs[UUID].signalMeter.dataset.cpu = "0";
}
}
}
if ("hw_enc" in miniInfo) {
session.rpcs[UUID].stats.info.hardware_video_encoder = miniInfo.hw_enc;
}
if ("bat" in miniInfo) {
if (typeof miniInfo.bat == "number") {
session.rpcs[UUID].stats.info.power_level = miniInfo.bat * 100;
} else {
session.rpcs[UUID].stats.info.power_level = null;
}
}
if ("chrg" in miniInfo) {
session.rpcs[UUID].stats.info.plugged_in = miniInfo.chrg;
}
if ("out" in miniInfo && "c" in miniInfo.out) {
session.rpcs[UUID].stats.info.total_outbound_p2p_connections = miniInfo.out.c;
if (session.showConnections && session.rpcs[UUID].connectionDetails) {
session.rpcs[UUID].connectionDetails.innerText = "🔗" + session.rpcs[UUID].stats.info.total_outbound_p2p_connections;
session.rpcs[UUID].connectionDetails.dataset.value = session.rpcs[UUID].stats.info.total_outbound_p2p_connections;
}
}
if (session.rpcs[UUID].batteryMeter) {
batteryMeterInfoUpdate(UUID);
}
}
function batteryMeterInfoUpdate(UUID) {
if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.power_level !== null) {
var level = session.rpcs[UUID].batteryMeter.querySelector(".battery-level");
if (level) {
var value = session.rpcs[UUID].stats.info.power_level;
if (value > 100) {
value = 100;
}
if (value < 0) {
value = 0;
}
level.style.height = parseInt(value) + "%";
if (value < 15) {
session.rpcs[UUID].batteryMeter.classList.remove("warn");
session.rpcs[UUID].batteryMeter.classList.add("alert");
} else if (value < 25) {
session.rpcs[UUID].batteryMeter.classList.remove("alert");
session.rpcs[UUID].batteryMeter.classList.add("warn");
} else {
session.rpcs[UUID].batteryMeter.classList.remove("alert");
session.rpcs[UUID].batteryMeter.classList.remove("warn");
}
if (value < 100) {
session.rpcs[UUID].batteryMeter.classList.remove("hidden");
}
//session.rpcs[UUID].batteryMeter.title = value+"% battery remaining";
session.rpcs[UUID].batteryMeter.title = parseInt(value) + "% battery remaining";
}
}
if (session.rpcs[UUID].stats.info && "plugged_in" in session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.plugged_in === false) {
session.rpcs[UUID].batteryMeter.dataset.plugged = "0";
session.rpcs[UUID].batteryMeter.classList.remove("hidden");
} else {
session.rpcs[UUID].batteryMeter.dataset.plugged = "1";
// add on
session.rpcs[UUID].batteryMeter.title = parseInt(value) + "% charging";
session.rpcs[UUID].batteryMeter.classList.add("hidden");
}
}
function setupGuestLabelControl(UUID) {
var labelID = getById("label_" + UUID);
if (labelID) {
labelID.classList.add("contolboxLabel");
labelID.dataset.UUID = UUID;
if (session.rpcs[UUID].label) {
labelID.innerText = session.rpcs[UUID].label; // Replace underscores with a Space when publishing to HTML. No Double spaces.
labelID.classList.remove("addALabel");
} else if (session.directorUUID === UUID) {
miniTranslate(labelID, "main-director");
labelID.classList.remove("addALabel");
} else if (session.directorList.indexOf(UUID) >= 0) {
miniTranslate(labelID, "co-director");
labelID.classList.remove("addALabel");
} else {
miniTranslate(labelID, "add-a-label");
labelID.classList.add("addALabel");
}
labelID.onclick = async function (ee) {
var oldlabel = ee.target.innerText;
if (session.rpcs[ee.target.dataset.UUID].label === false) {
oldlabel = "";
}
window.focus();
var newlabel = await promptAlt(getTranslation("new-display-name"), false, false, oldlabel);
if (newlabel !== null) {
newlabel = newlabel.trim();
if (newlabel == "") {
newlabel = false;
if (session.directorUUID === UUID) {
miniTranslate(ee.target, "main-director");
//ee.target.innerHTML = getTranslation("main-director");
ee.target.classList.remove("addALabel");
} else if (session.directorList.indexOf(UUID) >= 0) {
miniTranslate(ee.target, "co-director");
//ee.target.innerHTML = getTranslation("co-director");
ee.target.classList.remove("addALabel");
} else {
miniTranslate(ee.target, "add-a-label");
//ee.target.innerHTML = getTranslation("add-a-label");
ee.target.classList.add("addALabel");
}
} else {
ee.target.innerText = newlabel;
ee.target.classList.remove("addALabel");
}
var data = {};
data.UUID = ee.target.dataset.UUID;
data.changeLabel = true;
data.value = newlabel;
session.sendRequest(data, data.UUID);
pokeAPI("details", getDetailedState(session.rpcs[UUID].streamID));
}
};
}
updateAriaLabel(UUID);
}
function updateAriaLabel(UUID = false) {
if (!(UUID in session.rpcs)) {
return;
}
var element = document.getElementById("container_" + UUID);
if (!element) {
return;
}
var sid = "";
if (session.rpcs[UUID].streamID) {
sid = ": " + session.rpcs[UUID].streamID;
}
if (session.rpcs[UUID].label) {
element.setAttribute("aria-label", session.rpcs[UUID].label);
} else if (session.directorUUID === UUID) {
element.setAttribute("aria-label", (getTranslation("main-director") || "Main Director") + sid);
} else if (session.directorList.indexOf(UUID) >= 0) {
element.setAttribute("aria-label", (getTranslation("co-director") || "Co Director") + sid);
} else if (sid) {
element.setAttribute("aria-label", "ID" + sid);
} else {
element.setAttribute("aria-label", getTranslation("undefined") || "Undefined");
}
element.setAttribute("role", "region");
}
function updateLabelDirectors(UUID) {
var elements = getById("label_" + UUID);
if (elements) {
if (session.rpcs[UUID].label) {
elements.innerText = session.rpcs[UUID].label;
elements.classList.remove("addALabel");
} else if (session.directorUUID === UUID) {
miniTranslate(elements, "main-director");
elements.classList.remove("addALabel");
} else if (session.directorList.indexOf(UUID) >= 0) {
miniTranslate(elements, "co-director");
elements.classList.remove("addALabel");
} else {
miniTranslate(elements, "add-a-label");
elements.classList.add("addALabel");
}
}
updateAriaLabel(UUID);
}
function updateLabelDirectors2(UUID) {
var elements = getById("label_" + UUID);
if (elements) {
if (session.directorUUID === UUID) {
miniTranslate(elements, "main-director");
elements.classList.remove("addALabel");
} else if (session.directorList.indexOf(UUID) >= 0) {
miniTranslate(elements, "co-director");
elements.classList.remove("addALabel");
} else {
miniTranslate(elements, "add-a-label");
elements.classList.add("addALabel");
}
}
updateAriaLabel(UUID);
}
function directorCoDirectorColoring(UUID) {
if (UUID === session.directorUUID) {
try {
session.rpcs[UUID].stats.info.director = true;
getById("container_" + UUID).classList.add("directorBox");
} catch (e) {}
} else if (session.directorList.indexOf(UUID) >= 0) {
try {
session.rpcs[UUID].stats.info.coDirector = true;
addDirectorBlue(UUID);
} catch (e) {}
}
}
function addDirectorBlue(UUID) {
getById("container_" + UUID).classList.add("directorBlue");
}
function soloLinkGeneratorInit(UUID) {
document.querySelectorAll("container_" + UUID).forEach(ele => {
ele.querySelectorAll("[data-sololink]").forEach(ele2 => {
// value='" + soloLink + "' href='" + soloLink + "'/>" + soloLink + "
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) {
if (parseInt(value) === -1) {
value = Math.random() * (Math.random() * 2 - 1);
warnlog(value);
} else if (value === false) {
return source;
} else if (value === true) {
value = 90;
} else {
value = parseFloat(value / 90) - 1 || 0;
if (value < -1) {
value = -1;
}
if (value > 1) {
value = 1;
}
}
var gainNode = session.audioCtx.createGain();
session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode = gainNode;
gainNode.value = 1 - Math.abs(value) / 2; // the stereo panner seems to make things extra loud, so they clip. REDUCE IT.
source.connect(gainNode);
var panNode = session.audioCtx.createStereoPanner();
session.rpcs[UUID].inboundAudioPipeline[trackid].panNode = panNode;
panNode.pan.value = value;
gainNode.connect(panNode);
return panNode;
}
function adjustPan(UUID, value) {
if (value === true) {
value = Math.random() * (Math.random() * 2 - 1);
} else if (value === false) {
value = 0;
} else {
value = parseFloat(value / 90) - 1 || 0;
if (value < -1) {
value = -1;
}
if (value > 1) {
value = 1;
}
}
for (var trackid in session.rpcs[UUID].inboundAudioPipeline) {
if ("panNode" in session.rpcs[UUID].inboundAudioPipeline[trackid]) {
session.rpcs[UUID].inboundAudioPipeline[trackid].panNode.pan.setValueAtTime(value, session.audioCtx.currentTime);
}
if ("gainPanNode" in session.rpcs[UUID].inboundAudioPipeline[trackid]) {
session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode.setValueAtTime(1 - Math.abs(value) / 2, session.audioCtx.currentTime);
}
}
}
function addDelayNode(source, UUID, trackid) {
// append the delay Node to the track??? WOULD THIS WORK?
var delay = parseFloat(session.sync) || 0;
if (delay < 0) {
delay = 0;
}
if ((session.audioBuffer!==false) && session.audioBuffer >= 0) {
delay += parseFloat(session.audioBuffer);
} else if (session.buffer && session.buffer > 0) {
delay += parseFloat(session.buffer);
}
delay = delay / 1000;
session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode = session.audioCtx.createDelay(delay + 5); // 5 seconds additionally added for the purpose of flexibility
session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode.delayTime.value = delay; // delayTime takes it in seconds.
source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode);
log("added new delay node");
return session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode;
}
function createStyleCanvas(UUID) {
// append the delay Node to the track??? WOULD THIS WORK?
if (!session.rpcs[UUID].canvas) {
// just make sure that if using &effects or something, to null the canvas after use, else this won't trigger.
session.rpcs[UUID].canvas = document.createElement("canvas");
session.rpcs[UUID].canvas.dataset.UUID = UUID;
if (session.rpcs[UUID].streamID) {
session.rpcs[UUID].canvas.dataset.sid = session.rpcs[UUID].streamID;
}
session.rpcs[UUID].canvas.style.pointerEvents = "auto";
session.rpcs[UUID].canvasCtx = session.rpcs[UUID].canvas.getContext("2d", { alpha: session.alpha });
//
session.rpcs[UUID].canvas.addEventListener("click", function (e) {
// show stats of video if double clicked
log("clicked");
try {
var uid = e.currentTarget.dataset.UUID;
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
if (session.statsMenu !== false) {
if ("stats" in session.rpcs[uid]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, uid);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid);
}
}
e.stopPropagation();
return false;
} else if ("prePausedBandwidth" in session.rpcs[uid]) {
unPauseVideo(e.currentTarget);
}
} catch (e) {
errorlog(e);
}
});
if (session.statsMenu) {
if ("stats" in session.rpcs[UUID]) {
var [menu, innerMenu] = statsMenuCreator();
printViewStats(innerMenu, UUID);
menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID);
}
}
if (session.aspectRatio) {
if (session.aspectRatio == 1) {
session.rpcs[UUID].canvas.width = "720";
session.rpcs[UUID].canvas.height = "1280";
} else if (session.aspectRatio == 2) {
session.rpcs[UUID].canvas.width = "960";
session.rpcs[UUID].canvas.height = "960";
} else if (session.aspectRatio == 3) {
session.rpcs[UUID].canvas.width = "1280";
session.rpcs[UUID].canvas.height = "960";
}
} else {
session.rpcs[UUID].canvas.width = "1280";
session.rpcs[UUID].canvas.height = "720";
}
updateMixer();
return true;
} else {
return false;
}
}
function applyStyleEffect(UUID) {
if (!session.rpcs[UUID].canvas || !session.rpcs[UUID].canvasCtx) {
return;
}
/* session.rpcs[UUID].canvasContainer = document.createElement("div");
session.rpcs[UUID].canvasContainer.appendChild(session.rpcs[UUID].canvas);
session.rpcs[UUID].canvas.style = "width:100%;height:100%;display:block;";
session.rpcs[UUID].canvasContainer.appendChild(session.rpcs[UUID].videoElement); */
if (session.style == 3) {
// black
session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0, 0, 0)";
session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height);
} else if (session.style == 4) {
session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0, 0, 0)";
session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height);
} else if (session.style == 5) {
var r = Math.random() * 255;
var g = Math.random() * 255;
var b = Math.random() * 255;
session.rpcs[UUID].canvasCtx.fillStyle = "rgb(" + r + ", " + g + ", " + b + ")";
session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height);
} else if (session.style == 6) {
session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0,0,0)";
session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height);
var r = Math.random() * 150 + 50;
var g = Math.random() * 150 + 50;
var b = Math.random() * 150 + 50;
session.rpcs[UUID].canvasCtx.fillStyle = "rgb(" + r + ", " + g + ", " + b + ")";
session.rpcs[UUID].canvasCtx.beginPath();
session.rpcs[UUID].canvasCtx.arc(parseInt(session.rpcs[UUID].canvas.width / 2), parseInt(session.rpcs[UUID].canvas.height / 2), parseInt(session.rpcs[UUID].canvas.height / 4), 0, 2 * Math.PI, false);
session.rpcs[UUID].canvasCtx.fill();
if (session.rpcs[UUID].label) {
session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0,0,0)";
session.rpcs[UUID].canvasCtx.textAlign = "center";
session.rpcs[UUID].canvasCtx.font = parseInt(session.rpcs[UUID].canvas.height / 2.11) + "px Arial";
session.rpcs[UUID].canvasCtx.fillText(session.rpcs[UUID].label[0].toUpperCase(), parseInt(session.rpcs[UUID].canvas.width / 2), parseInt((session.rpcs[UUID].canvas.height * 2) / 3));
} else {
var tmp = getComputedStyle(document.querySelector(":root")).getPropertyValue("--video-background-image").split('"');
if (tmp.length === 3) {
var img = new Image();
img.onload = function () {
session.rpcs[UUID].canvasCtx.fillStyle = "rgb(25,0,0)";
session.rpcs[UUID].canvasCtx.drawImage(img, parseInt(session.rpcs[UUID].canvas.width / 2 - 110), parseInt(session.rpcs[UUID].canvas.height / 2 - 110), 220, 220);
};
img.src = tmp[1];
}
}
}
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function fftWaveform(source, UUID, trackid) {
// append the delay Node to the track??? WOULD THIS WORK?
// https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser = session.audioCtx.createAnalyser();
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.fftSize = 512;
var bufferLength = session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteTimeDomainData(dataArray);
// analyser.getByteTimeDomainData(dataArray);
source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser);
createStyleCanvas(UUID);
clearInterval(session.rpcs[UUID].canvasIntervalAction);
var canvasIntervalAction = setInterval(
function (uuid) {
if (session.style !== 2) {
clearInterval(canvasIntervalAction); // this is FFT only, so okay to kill.
return;
}
try {
session.rpcs[uuid].inboundAudioPipeline[trackid].analyser.getByteTimeDomainData(dataArray);
session.rpcs[uuid].canvasCtx.fillStyle = "rgba(0, 0, 0, 0.1)";
session.rpcs[uuid].canvasCtx.fillRect(0, 0, session.rpcs[uuid].canvas.width, session.rpcs[uuid].canvas.height);
session.rpcs[uuid].canvasCtx.lineWidth = 10;
session.rpcs[uuid].canvasCtx.strokeStyle = "rgb(111, 255, 111)";
var sliceWidth = (session.rpcs[uuid].canvas.width * 1.0) / bufferLength;
var loudness = dataArray;
var Squares = loudness.map(val => (val - 128.0) * (val - 128.0));
var Sum = Squares.reduce((acum, val) => acum + val);
var Mean = Sum / loudness.length;
loudness = Math.sqrt(Mean) * 10;
session.rpcs[uuid].stats.Audio_Loudness = parseInt(loudness);
if (session.pushLoudness == true) {
var loudnessObj = {};
loudnessObj[session.rpcs[uuid].streamID] = session.rpcs[uuid].stats.Audio_Loudness;
if (isIFrame) {
parent.postMessage({ loudness: loudnessObj, action: "loudness", value: loudness, UUID: uuid }, session.iframetarget);
}
}
if (loudness < 2) {
return;
}
//log(bufferLength);
session.rpcs[uuid].canvasCtx.beginPath();
var m = session.rpcs[uuid].canvas.height / 256.0;
session.rpcs[uuid].canvasCtx.moveTo(0, dataArray[0] * m);
var x = 0;
for (var i = 1; i < bufferLength; i++) {
var y = dataArray[i] * m;
session.rpcs[uuid].canvasCtx.lineTo(x, y);
x += sliceWidth;
}
session.rpcs[uuid].canvasCtx.lineTo(session.rpcs[uuid].canvas.width, session.rpcs[uuid].canvas.height / 2);
session.rpcs[uuid].canvasCtx.stroke();
} catch (e) {
warnlog(e);
warnlog("Did the remote source disconnect?");
clearInterval(canvasIntervalAction);
warnlog(session.rpcs[uuid]);
}
},
50,
UUID
);
session.rpcs[UUID].canvasIntervalAction = canvasIntervalAction;
return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser;
}
function audioMeterGuest(mediaStreamSource, UUID, trackid) {
log("audioMeterGuest started");
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser = session.audioCtx.createAnalyser();
mediaStreamSource.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser);
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.fftSize = 256;
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.smoothingTimeConstant = 0.05;
var bufferLength = session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
function updateLevels() {
try {
if (!session.rpcs[UUID]) {
return;
}
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteFrequencyData(dataArray);
var total = 0;
for (var i = 0; i < dataArray.length; i++) {
total += dataArray[i];
}
total = parseInt(total / 150);
session.rpcs[UUID].stats.Audio_Loudness = total;
if (session.pushLoudness == true) {
var loudnessObj = {};
loudnessObj[session.rpcs[UUID].streamID] = session.rpcs[UUID].stats.Audio_Loudness;
if (isIFrame) {
parent.postMessage({ loudness: loudnessObj, action: "loudness", value: session.rpcs[UUID].stats.Audio_Loudness, UUID: UUID }, session.iframetarget);
}
}
try {
clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () {
updateLevels();
}, 100);
} catch (e) {
log("closing old inaudio pipeline");
}
if (session.style == 3 || session.meterStyle) {
// overrides style
if (session.rpcs[UUID].videoElement) {
if (total > 40) {
session.rpcs[UUID].videoElement.dataset.speaking = "2";
} else if (total > 10) {
session.rpcs[UUID].videoElement.dataset.speaking = "1";
} else {
session.rpcs[UUID].videoElement.dataset.speaking = "0";
}
if (session.meterStyle == 4) {
session.rpcs[UUID].videoElement.dataset.loudness = total;
return; // this is cause we are using the data-loudness
}
} else if (session.meterStyle == 4) {
return;
}
} else if (session.scene !== false) {
// if a scene, cancel
return;
} else if (session.audioMeterGuest === false) {
// don't show if we just want the volume levels
return;
}
if (session.rpcs[UUID].voiceMeter) {
session.rpcs[UUID].voiceMeter.dataset.level = total;
if (session.meterStyle == 1) {
var perct = Math.min(total, 100);
session.rpcs[UUID].voiceMeter.style.height = perct + "%";
if (total > 80) {
var R = parseInt((255 * perct) / 100)
.toString(16)
.padStart(2, "0");
var G = parseInt(255 - (255 * perct) / 100)
.toString(16)
.padStart(2, "0");
session.rpcs[UUID].voiceMeter.style.backgroundColor = "#" + R + G + "00";
} else {
session.rpcs[UUID].voiceMeter.style.backgroundColor = "#00FF00";
}
} else {
if (total > 15) {
session.rpcs[UUID].voiceMeter.style.opacity = 100; // temporary
} else {
session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary
}
}
} else {
session.rpcs[UUID].voiceMeter = document.createElement("div");
session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID;
session.rpcs[UUID].voiceMeter.dataset.level = total;
if (session.meterStyle == 1) {
session.rpcs[UUID].voiceMeter.classList.add("video-meter2");
} else {
if (total > 15) {
session.rpcs[UUID].voiceMeter.style.opacity = 100; // temporary
} else {
session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary
}
if (session.meterStyle == 2) {
session.rpcs[UUID].voiceMeter.classList.add("video-meter-2");
} else {
session.rpcs[UUID].voiceMeter.classList.add("video-meter");
}
}
updateMixer();
}
} catch (e) {
warnlog(e);
// fail as an exception; this is a control close.
return;
}
}
clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval);
session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () {
updateLevels();
}, 100);
return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser;
}
function effectsDynamicallyUpdate(event, ele) {
log("effectsDynamicallyUpdate");
let lastEffectValue = session.effect;
session.effect = ele.options[ele.selectedIndex].value;
getById("selectImageContent").style.display = "none";
getById("selectImageContent3").style.display = "none";
getById("selectImageOverlay").style.display = "none";
getById("selectImageOverlay3").style.display = "none";
getById("selectEffectAmount").style.display = "none";
getById("selectEffectAmount3").style.display = "none";
if (session.effect === "1") {
updateRenderOutpipe();
return;
}
if (session.effect === "7") {
// Show zoom amount sliders
getById("selectEffectAmount").style.display = "block";
getById("selectEffectAmount3").style.display = "block";
// Show both sets of position controls
getById("zoomPositionControls").style.display = "block";
getById("zoomPositionControls3").style.display = "block";
if (session.effectValue_default) {
session.effectValue = session.effectValue_default;
} else {
session.effectValue = 1;
}
// Set up zoom amount sliders
const zoomInputs = ["selectEffectAmountInput", "selectEffectAmountInput3"];
zoomInputs.forEach(id => {
const input = getById(id);
if (input) {
input.min = 1;
input.max = 4;
input.step = 0.1;
input.value = session.effectValue;
}
});
// Initialize all position sliders
const sliderPairs = [
["zoomPositionX1", "zoomPositionX"],
["zoomPositionY1", "zoomPositionY"]
];
sliderPairs.forEach(pair => {
pair.forEach(id => {
const slider = getById(id);
if (slider) {
if (id.includes('X')) {
slider.value = xPosition;
} else {
slider.value = yPosition;
}
}
});
});
} else {
// Hide all position controls
getById("zoomPositionControls").style.display = "none";
getById("zoomPositionControls3").style.display = "none";
}
if (["8", "overlay"].includes(session.effect)) {
// like zoom but none
if (session.effect === "overlay") {
loadOverlayImages();
}
updateRenderOutpipe();
return;
}
if (session.effect == "3a") {
session.effect = "3";
session.effectValue = 5;
}
if (session.effectValue_default === false && session.effect == "3") {
session.effectValue = 2;
} else {
session.effectValue = session.effectValue_default;
}
if (session.effect == "0" || !session.effect) {
updateRenderOutpipe();
return;
} else if (session.effect === "3" || session.effect === "4") {
if (!["3", "4", "5"].includes(lastEffectValue)) {
attemptTFLiteJsFileLoad();
if (!session.tfliteModule.looping) {
updateRenderOutpipe();
}
}
if (session.effect === "3" && session.effectValue_default == false) {
getById("selectEffectAmount").style.display = "block";
getById("selectEffectAmount3").style.display = "block";
getById("selectEffectAmountInput").min = 0;
getById("selectEffectAmountInput").max = 20;
getById("selectEffectAmountInput").step = 1;
getById("selectEffectAmountInput3").min = 0;
getById("selectEffectAmountInput3").max = 20;
getById("selectEffectAmountInput3").step = 1;
getById("selectEffectAmountInput").value = session.effectValue;
getById("selectEffectAmountInput3").value = session.effectValue;
}
} else if (session.effect === "5") {
if (!["3", "4", "5"].includes(lastEffectValue)) {
attemptTFLiteJsFileLoad();
if (!session.tfliteModule.looping) {
updateRenderOutpipe();
}
}
loadContentEffectsImages();
} else if ((session.effect === "14" || session.effect === "15") && session.effectValue_default == false) {
getById("selectEffectAmount").style.display = "block";
getById("selectEffectAmount3").style.display = "block";
getById("selectEffectAmountInput").min = 1;
getById("selectEffectAmountInput").max = 50;
getById("selectEffectAmountInput").step = 1;
getById("selectEffectAmountInput3").min = 1;
getById("selectEffectAmountInput3").max = 50;
getById("selectEffectAmountInput3").step = 1;
getById("selectEffectAmountInput").value = parseInt(session.effectValue) || 25;
getById("selectEffectAmountInput3").value = parseInt(session.effectValue) || 25;
loadContentEffectsImages();
} else if (session.effect === "6") {
if (!gpgpuSupport) {
if (!session.cleanOutput) {
warnUser("Hardware acceleration isn't detected.
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.