Last active
February 17, 2022 07:49
-
-
Save llehle/88644e89646a8d857f4e9b5ecef69ee7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name BBB NotSoSilent Observer | |
// @version 3 | |
// @grant none | |
// ==/UserScript== | |
// based on BBB Userlist diff | |
// https://gist.github.com/chris246/3f5e46d37d60e92f64b48dc9fb0dc518 | |
///////////////////////////////////////////////////////////////////// | |
// How to: | |
// * Install userscript (and refresh bbb) | |
// * Insert own bbb display name below such that you dont appear in supervision listings | |
// * To declutter the log: Filter for `>>` and deselect 'debug' | |
///////////////////////////////////////////////////////////////////// | |
// Features: | |
// * Console log shows joining/leaving users and activated/deativated cams with timestamp | |
// * Supervision group in sidepanel with two actions: | |
// * Cam Status: list of all unique participants* with a hint if they have no camera active | |
// * Missing cams: list of all unique participants* without active camera | |
// * Supervision blinks red if a camera feed dropped (can be acknowledged with click on supervision or on one of the two actions) | |
// * unique participants are reduced from the participants list by removing `*` (with leading and tailing spaces) | |
///////////////////////////////////////////////////////////////////// | |
// Known problems: | |
// * No notification/log message is created when the last camera feed drops, because bbb removes the container and the observer stops working. | |
///////////////////////////////////////////////////////////////////// | |
// Insert own name here to have it filtered out in listings | |
const ownName = "Your name goes here"; | |
///////////////////////////////////////////////////////////////////// | |
const delay = 1000; | |
const selSidepanel = "div[class^='userList--'] div[class^='userList--'] div[class^='content--']"; | |
const selParticipantsList = "div[class^='userListColumn--'] div[class^='scrollableList--'] div[class^='list--']"; | |
const _selParticipant = " div[class^='userName--'] span[class^='userNameMain--'] span"; | |
// const selVideoList = "div[class^='overlay--'] div[class^='videoCanvas--'] div[class^='videoList--']"; // disappears if all videos disconnect | |
const selVideoList = "div[class^='videoCanvas--'] div[class^='videoList--']"; // like above, with fix for if presentation minimized | |
// const selVideoList = "div[class^='content--'] section[class^='media--'] div[class^='container--']"; // alternative search in whole right area | |
const _selVideoConnecting = " div[class^='connecting--'] span[class^='loadingText--']"; | |
const _selVideoStream = " div[class^='videoListItem--'] div[class^='info--'] span[class^='dropdownTrigger--']"; | |
//insert keyframes for attention animation | |
var style = document.createElement('style'); | |
var keyFrames = '\ | |
@-webkit-keyframes zooming {\ | |
0% {\ | |
transform: scale(1,1) translate(0px,0px);\ | |
}\ | |
100% {\ | |
transform: scale(2,2) translate(40px,0px);\ | |
}\ | |
}\ | |
@-moz-keyframes zooming {\ | |
0% {\ | |
transform: scale(1,1) translate(0px,0px);\ | |
}\ | |
100% {\ | |
transform: scale(2,2) translate(40px,0px);\ | |
}\ | |
}'; | |
style.innerHTML = keyFrames; | |
document.getElementsByTagName('head')[0].appendChild(style); | |
var observerParticipants = new MutationObserver(ms => ms.forEach(updateLists)); | |
var observerVideos = new MutationObserver(ms => ms.forEach(updateLists)); | |
var containerParticipants; | |
var containerVideo; | |
var supervisionContainer; | |
var supervisionHeading; | |
var participantsLast = []; | |
var participantsUnique = []; | |
var videosLast = []; | |
var videosUnique = []; | |
console.debug(">> BBB Observer started"); | |
setInterval(createSupervisionButtons, delay); | |
setInterval(registerParticipantObserver, delay); | |
setInterval(registerVideoObserver, delay); | |
/** | |
* Periorically called. | |
* Inserts supervision buttons in sidepanel if they not already exist. | |
*/ | |
function createSupervisionButtons() { | |
// check if sidepanel there; otherwise retry | |
var sidepanel = document.querySelectorAll(selSidepanel)[0]; | |
if (sidepanel == undefined) | |
return; | |
//check if supervision already inserted | |
if (document.querySelectorAll(".supervision--c0ffee").length > 0) | |
return; | |
// insert supervision buttons | |
console.debug(">> Creating supervision buttons"); | |
supervisionContainer = sidepanel.firstChild.cloneNode(true); //clone "Messages/Public Chat" | |
supervisionContainer.classList.add("supervision--c0ffee"); | |
supervisionHeading = supervisionContainer.querySelectorAll("div h2")[0]; | |
supervisionHeading.innerHTML = "Supervision"; //change title | |
supervisionHeading.addEventListener("click", attentionClear); | |
sidepanel.insertBefore(supervisionContainer, sidepanel.children[2]); //insert before participants list | |
//create template, remove unnecessary stuff | |
var btnTemplate = supervisionContainer.querySelectorAll("div[class^=chatListItem--]")[0]; | |
removeClassPrefix(btnTemplate, "active"); //remove active class /public chat (where this is cloned from) is active by default | |
btnTemplate.querySelectorAll("div[class^=chatIcon--] div")[0].innerHTML = ""; | |
btnTemplate.querySelectorAll("div[class^=chatName--] span")[0].classList.add("btn-display-text"); | |
btnTemplate.parentNode.remove(btnTemplate); | |
//display list of participants and camera status | |
var btnCamStatus = btnTemplate.cloneNode(true); | |
btnCamStatus.querySelectorAll(".btn-display-text")[0].innerHTML = "Cam Status"; | |
btnCamStatus.addEventListener("click", () => { | |
attentionClear(); | |
updateLists(); | |
var out = participantsUnique // | |
.sort() // | |
.filter(p => p != ownName) // | |
.map(p => (!videosUnique.includes(p) ? "No cam ->\t" : "\t\t\t") + p).join("\n"); | |
alert(out); | |
}); | |
supervisionContainer.appendChild(btnCamStatus); | |
//display list missing cameras | |
var btnMissingCam = btnTemplate.cloneNode(true); | |
btnMissingCam.querySelectorAll(".btn-display-text")[0].innerHTML = "Missing Cams"; | |
btnMissingCam.addEventListener("click", () => { | |
attentionClear(); | |
updateLists(); | |
var out = participantsUnique // | |
.sort() // | |
.filter(p => p != ownName) // | |
.filter(p => !videosUnique.includes(p))// | |
.join("\n"); | |
alert(out.length == 0 ? "All participants joined with camera" : out); | |
}); | |
supervisionContainer.appendChild(btnMissingCam); | |
} | |
/** | |
* Start attention animation on supervision header. | |
*/ | |
function attention() { | |
if (supervisionHeading != undefined) | |
supervisionHeading.setAttribute("style", "font-weight: bold; color: red; animation: zooming 1s ease-in-out infinite; animation-direction: alternate"); | |
} | |
/** | |
* Cleat attention animation from supervision header. | |
*/ | |
function attentionClear() { | |
if (supervisionHeading != undefined) | |
supervisionHeading.setAttribute("style", ""); | |
} | |
/** | |
* Periorically called. | |
* Check if container is still the same (if present) and registers the observer on it if not. | |
*/ | |
function registerParticipantObserver() { | |
var oldContainer = containerParticipants; | |
containerParticipants = document.querySelectorAll(selParticipantsList + " div")[0]; | |
if (containerParticipants == oldContainer || containerParticipants == undefined) | |
return; | |
console.debug(">> Registering participant observer"); | |
participantsLast = []; // clear previous state, used if container is recreated | |
updateLists(); // initial update | |
observerParticipants.disconnect(); | |
observerParticipants.observe(containerParticipants, { childList: true }); | |
} | |
/** | |
* Periorically called. | |
* Check if container is still the same (if present) and registers the observer on it if not. | |
*/ | |
function registerVideoObserver() { | |
var oldContainer = containerVideo; | |
containerVideo = document.querySelectorAll(selVideoList)[0]; | |
if (containerVideo == oldContainer || containerVideo == undefined) | |
return; | |
console.debug(">> Registering video observer"); | |
videosLast = []; // clear previous state, used if container is recreated | |
updateLists(); // initial update | |
observerVideos.disconnect(); | |
observerVideos.observe(containerVideo, { childList: true }); | |
} | |
function updateLists() { | |
console.debug(">> Update lists called"); | |
var participantsNow = []; | |
var videosNow = []; | |
var re = RegExp(" $"); | |
//retrieve current participants list from any user lists | |
Array.from(document.querySelectorAll(selParticipantsList + _selParticipant)) | |
.forEach(e => participantsNow.push(e.innerHTML.replace(re, ''))); | |
participantsUnique = [...new Set(participantsNow.map(e => e.replace(/\s*\*\s*/, "")))]; | |
//retrieve current videos list | |
Array.from(document.querySelectorAll(selVideoList + _selVideoConnecting + ", " + selVideoList + _selVideoStream)) | |
.forEach(e => videosNow.push(e.innerHTML.replace(re, ''))); | |
videosUnique = [...new Set(videosNow.map(e => e.replace(/\s*\*\s*/, "")))]; | |
//skip if no changes detected | |
if (participantsNow.sameElements(participantsLast) && videosNow.sameElements(videosLast)) | |
return; | |
// format timestamp | |
var date = new Date(); | |
var displaytime = date.getHours() + ":" + date.getMinutes().pad(2) + ":" + date.getSeconds().pad(2); | |
// detect and print joined users if there were any | |
var joinedParticipants = participantsNow.without(participantsLast).join(", "); | |
if (joinedParticipants.length != 0) | |
console.log(">> " + displaytime + " > Joined: " + joinedParticipants); | |
// detect and print left users if there were any | |
var leftParticipants = participantsLast.without(participantsNow).join(", "); | |
if (leftParticipants.length != 0) | |
console.log(">> " + displaytime + " > Left: " + leftParticipants); | |
// detect and print cameras that were activated | |
var activatedCameras = videosNow.without(videosLast).join(", "); | |
if (activatedCameras.length != 0) | |
console.log(">> " + displaytime + " > Video activated: " + activatedCameras); | |
// detect and print cameras that were activated | |
var deactivatedCameras = videosLast.without(videosNow).join(", "); | |
if (deactivatedCameras.length != 0) { | |
console.log(">> " + displaytime + " > Video deactivated: " + deactivatedCameras); | |
attention(); | |
} | |
//store lists for next modification detection | |
participantsLast = participantsNow.slice(); | |
videosLast = videosNow.slice(); | |
} | |
Number.prototype.pad = function (size) { | |
var s = String(this); | |
while (s.length < (size || 2)) { s = "0" + s; } | |
return s; | |
} | |
Array.prototype.without = function (otherArray) { | |
var temp = []; | |
this.forEach(function (e) { | |
if (!otherArray.includes(e)) { | |
temp.push(e); | |
} | |
}); | |
return temp; | |
} | |
Array.prototype.intersect = function (otherArray) { | |
return this.filter(function (n) { return otherArray.indexOf(n) !== -1; }) | |
} | |
Array.prototype.sameElements = function (otherArray) { | |
return JSON.stringify(this) === JSON.stringify(otherArray); | |
} | |
function removeClassPrefix(el, clsprefix) { | |
for (let i = el.classList.length - 1; i >= 0; i--) { | |
const className = el.classList[i]; | |
if (className.startsWith(clsprefix)) { | |
el.classList.remove(className); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment