Skip to content

Instantly share code, notes, and snippets.

@chris246
Forked from llehle/bbb-observer.js
Created February 17, 2022 07:49
Show Gist options
  • Save chris246/4fd409538eca3da14b3ed37aa30f00c9 to your computer and use it in GitHub Desktop.
Save chris246/4fd409538eca3da14b3ed37aa30f00c9 to your computer and use it in GitHub Desktop.
// ==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