'use strict';
/**
* MiroTalk SFU - Client component
*
* @link GitHub: https://github.com/miroslavpejic85/mirotalksfu
* @link Official Live demo: https://sfu.mirotalk.com
* @license For open source use: AGPLv3
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.9.96
*
*/
const cfg = {
useAvatarSvg: true,
};
const html = {
newline: '\n', //'
',
hideMeOn: 'fas fa-user-slash',
hideMeOff: 'fas fa-user',
audioOn: 'fas fa-microphone',
audioOff: 'fas fa-microphone-slash',
videoOn: 'fas fa-video',
videoOff: 'fas fa-video-slash',
userName: 'username',
userHand: 'fas fa-hand-paper user-hand pulsate',
pip: 'fas fa-images',
fullScreen: 'fas fa-expand',
fullScreenOn: 'fas fa-compress-alt',
fullScreenOff: 'fas fa-expand-alt',
snapshot: 'fas fa-camera-retro',
sendFile: 'fas fa-upload',
sendMsg: 'fas fa-paper-plane',
sendVideo: 'fab fa-youtube',
geolocation: 'fas fa-location-dot',
ban: 'fas fa-ban',
kickOut: 'fas fa-times',
ghost: 'fas fa-ghost',
undo: 'fas fa-undo',
bg: 'fas fa-circle-half-stroke',
pin: 'fas fa-map-pin',
videoPrivacy: 'far fa-circle',
expand: 'fas fa-bars dropdown-button',
hideALL: 'fas fa-eye',
mirror: 'fas fa-arrow-right-arrow-left',
close: 'fas fa-times',
stop: 'fas fa-circle-stop',
};
const icons = {
room: '',
chat: '',
user: '',
transcript: '',
speech: '',
share: '',
ptt: '',
lobby: '',
lock: '',
unlock: '',
pitchBar: '',
mirror: '',
sounds: '',
fileSend: '',
fileReceive: '',
recording: '',
moderator: '',
broadcaster: '',
codecs: '',
theme: '',
recSync: '',
refresh: '',
editor: '',
up: '',
down: '',
};
const image = {
about: '../images/mirotalk-logo.gif',
avatar: '../images/mirotalksfu-logo.png',
audio: '../images/audio.gif',
poster: '../images/loader.gif',
rec: '../images/rec.png',
recording: '../images/recording.png',
delete: '../images/delete.png',
locked: '../images/locked.png',
mute: '../images/mute.png',
hide: '../images/hide.png',
stop: '../images/stop.png',
unmute: '../images/unmute.png',
unhide: '../images/unhide.png',
start: '../images/start.png',
users: '../images/participants.png',
user: '../images/participant.png',
username: '../images/user.png',
videoShare: '../images/video-share.png',
message: '../images/message.png',
share: '../images/share.png',
exit: '../images/exit.png',
feedback: '../images/feedback.png',
lobby: '../images/lobby.png',
email: '../images/email.png',
chatgpt: '../images/chatgpt.png',
deepSeek: '../images/deepSeek.png',
all: '../images/all.png',
forbidden: '../images/forbidden.png',
broadcasting: '../images/broadcasting.png',
geolocation: '../images/geolocation.png',
network: '../images/network.gif',
rtmp: '../images/rtmp.png',
save: '../images/save.png',
transcription: '../images/transcription.png',
back: '../images/back.png',
blur: '../images/blur.png',
blurLow: '../images/blur-low.png',
blurHigh: '../images/blur-high.png',
transparentBg: '../images/transparentBg.png',
link: '../images/link.png',
upload: '../images/upload.png',
virtualBackground: {
one: '../images/virtual-background/default/background-1.jpg',
two: '../images/virtual-background/default/background-2.webp',
three: '../images/virtual-background/default/background-3.jpg',
four: '../images/virtual-background/default/background-4.jpg',
five: '../images/virtual-background/default/background-5.jpg',
six: '../images/virtual-background/default/background-6.jpg',
seven: '../images/virtual-background/default/background-7.jpg',
eight: '../images/virtual-background/default/background-8.jpg',
nine: '../images/virtual-background/default/background-9.jpg',
ten: '../images/virtual-background/default/background-10.jpg',
eleven: '../images/virtual-background/default/background-11.gif',
},
};
const mediaType = {
audio: 'audioType',
audioTab: 'audioTab',
video: 'videoType',
camera: 'cameraType',
screen: 'screenType',
speaker: 'speakerType',
};
const _EVENTS = {
openRoom: 'openRoom',
exitRoom: 'exitRoom',
startRec: 'startRec',
pauseRec: 'pauseRec',
resumeRec: 'resumeRec',
stopRec: 'stopRec',
raiseHand: 'raiseHand',
lowerHand: 'lowerHand',
startVideo: 'startVideo',
pauseVideo: 'pauseVideo',
resumeVideo: 'resumeVideo',
stopVideo: 'stopVideo',
startAudio: 'startAudio',
pauseAudio: 'pauseAudio',
resumeAudio: 'resumeAudio',
stopAudio: 'stopAudio',
startScreen: 'startScreen',
pauseScreen: 'pauseScreen',
resumeScreen: 'resumeScreen',
stopScreen: 'stopScreen',
roomLock: 'roomLock',
lobbyOn: 'lobbyOn',
lobbyOff: 'lobbyOff',
roomUnlock: 'roomUnlock',
hostOnlyRecordingOn: 'hostOnlyRecordingOn',
hostOnlyRecordingOff: 'hostOnlyRecordingOff',
startRTMP: 'startRTMP',
stopRTMP: 'stopRTMP',
endRTMP: 'endRTMP',
startRTMPfromURL: 'startRTMPfromURL',
stopRTMPfromURL: 'stopRTMPfromURL',
endRTMPfromURL: 'endRTMPfromURL',
};
// Enums
const enums = {
recording: {
started: 'Started conference recording',
start: 'Start conference recording',
stop: 'Stop conference recording',
},
//...
};
// HeyGen config
const VideoAI = {
enabled: true,
active: false,
info: {},
avatarId: null,
avatarName: 'Monica',
avatarVoice: null,
quality: 'medium',
virtualBackground: true,
background: image.virtualBackground.one,
};
// Recording
let recordedBlobs = [];
class RoomClient {
constructor(
localAudioEl,
remoteAudioEl,
videoMediaContainer,
videoPinMediaContainer,
mediasoupClient,
socket,
room_id,
peer_name,
peer_uuid,
peer_info,
isAudioAllowed,
isVideoAllowed,
isScreenAllowed,
joinRoomWithScreen,
isSpeechSynthesisSupported,
transcription,
successCallback
) {
this.room_id = room_id;
this.peer_id = socket.id;
this.peer_name = peer_name;
this.peer_uuid = peer_uuid;
this.peer_info = peer_info;
this.peer_avatar = peer_info.peer_avatar;
// Device type
this.isDesktopDevice = peer_info.is_desktop_device;
this.isMobileDevice = peer_info.is_mobile_device;
this.isMobileSafari = this.isMobileDevice && peer_info.browser_name.toLowerCase().includes('safari');
this.pendingSinkId = null; // store desired sink id until next user gesture
this.localAudioEl = localAudioEl;
this.remoteAudioEl = remoteAudioEl;
this.videoMediaContainer = videoMediaContainer;
this.videoPinMediaContainer = videoPinMediaContainer;
this.mediasoupClient = mediasoupClient;
// Handle Socket
this.socket = socket;
this.reconnectAlert = null;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 3000;
this.maxReconnectInterval = 15000;
this.serverAwayShown = false;
this.silentReconnect = false; // If true, no popup will be shown on reconnect
// Handle ICE
this.iceRestarting = false;
this.iceProducerRestarting = false;
this.iceConsumerRestarting = false;
// RTMP selected file name
this.selectedRtmpFilename = '';
// Moderator
this._moderator = {
video_start_privacy: false,
audio_start_muted: false,
video_start_hidden: false,
audio_cant_unmute: false,
video_cant_unhide: false,
screen_cant_share: false,
chat_cant_privately: false,
chat_cant_chatgpt: false,
chat_cant_deep_seek: false,
media_cant_sharing: false,
};
// Chat messages
this.chatMessageLengthCheck = false;
this.chatMessageLength = 4000; // chars
this.chatMessageTimeLast = 0;
this.chatMessageTimeBetween = 1000; // ms
this.chatMessageNotifyDelay = 10000; // ms
this.chatMessageSpamCount = 0;
this.chatMessageSpamCountToBan = 10;
this.chatPeerId = 'all';
this.chatPeerName = 'all';
this.chatPeerAvatar = '';
// HeyGen Video AI
this.videoAIContainer = null;
this.videoAIElement = null;
this.canvasAIElement = null;
this.renderAIToken = null;
this.peerConnection = null;
this.dominantSpeaker = false;
this.isAudioAllowed = isAudioAllowed;
this.isVideoAllowed = isVideoAllowed;
this.isScreenAllowed = isScreenAllowed;
this.joinRoomWithScreen = joinRoomWithScreen;
this.producerTransport = null;
this.consumerTransport = null;
this.device = null;
this.isScreenShareSupported =
navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia ? true : false;
this.isMySettingsOpen = false;
this._isConnected = false;
this.isVideoBarDropDownOpen = false;
this.isDocumentOnFullScreen = false;
this.isVideoOnFullScreen = false;
this.isVideoFullScreenSupported = this.isFullScreenSupported();
this.isVideoPictureInPictureSupported = document.pictureInPictureEnabled;
this.isZoomCenterMode = false;
this.isChatOpen = false;
this.isChatEmojiOpen = false;
this.isPollOpen = false;
this.isPollPinned = false;
this.isEditorOpen = false;
this.isEditorLocked = false;
this.isEditorPinned = false;
this.isSpeechSynthesisSupported = isSpeechSynthesisSupported;
this.speechInMessages = false;
this.showChatOnMessage = true;
this.isChatBgTransparent = false;
this.isVideoPinned = false;
this.isChatPinned = false;
this.isChatMaximized = false;
this.isToggleUnreadMsg = false;
this.isToggleRaiseHand = false;
this.pinnedVideoPlayerId = null;
this.camVideo = false;
this.videoQualitySelectedIndex = 0;
this.pollSelectedOptions = {};
this.chatGPTContext = [];
this.deepSeekContext = [];
this.chatMessages = [];
this.leftMsgAvatar = null;
this.rightMsgAvatar = null;
this.localVideoElement = null;
this.localVideoStream = null;
this.localAudioStream = null;
this.localScreenStream = null;
// Room Password
this.RoomIsLocked = false;
this.RoomPassword = false;
this.RoomPasswordValid = false;
// Room Lobby
this.RoomIsLobby = false;
this.RoomLobbyAccepted = false;
this.lobbyPears = {};
this.transcription = transcription;
// RTMP Streamer
this.rtmpFileStreamer = false;
this.rtmpUrltSreamer = false;
// File transfer settings
this.fileToSend = null;
this.fileReader = null;
this.receiveBuffer = [];
this.receivedSize = 0;
this.incomingFileInfo = null;
this.incomingFileData = null;
this.sendInProgress = false;
this.receiveInProgress = false;
this.fileSharingInput = '*';
this.chunkSize = 1024 * 16; // 16kb/s
// Recording
this._isRecording = false;
this._recStartTs = null;
this.mediaRecorder = null;
this.audioRecorder = null;
this.recScreenStream = null;
this.recording = {
recSyncServerRecording: false,
recSyncServerToS3: false,
recSyncServerEndpoint: '',
};
this.recSyncTime = 4000; // 4 sec
this.recSyncChunkSize = 1000000; // 1MB
// Encodings
this.preferLocalCodecsOrder = false; // Prefer local codecs order
this.forceVP8 = false; // Force VP8 codec for webcam and screen sharing
this.forceVP9 = false; // Force VP9 codec for webcam and screen sharing
this.forceH264 = false; // Force H264 codec for webcam and screen sharing
this.forceAV1 = false; // Force AV1 codec for webcam and screen sharing
this.enableWebcamLayers = true; // Enable simulcast or SVC for webcam
this.enableSharingLayers = true; // Enable simulcast or SVC for screen sharing
this.numSimulcastStreamsWebcam = 3; // Number of streams for simulcast in webcam
this.numSimulcastStreamsSharing = 1; // Number of streams for simulcast in screen sharing
this.webcamScalabilityMode = 'L3T3'; // Scalability Mode for webcam | 'L1T3' for VP8/H264 (in each simulcast encoding), 'L3T3_KEY' for VP9
this.sharingScalabilityMode = 'L1T3'; // Scalability Mode for screen sharing | 'L1T3' for VP8/H264 (in each simulcast encoding), 'L3T3' for VP9
this.myVideoEl = null;
this.myAudioEl = null;
this.showPeerInfo = false; // on peerName mouse hover show additional info
// Noise Suppression
this.RNNoiseProcessor = null;
this.videoProducerId = null;
this.screenProducerId = null;
this.audioProducerId = null;
this.audioConsumers = new Map();
this.peers = new Map();
this.consumers = new Map();
this.producers = new Map();
this.producerLabel = new Map();
this.eventListeners = new Map();
this.debug = false;
this.debug ? window.localStorage.setItem('debug', 'mediasoup*') : window.localStorage.removeItem('debug');
console.log('06 ----> Load MediaSoup Client v', mediasoupClient.version);
console.log('06.1 ----> PEER_ID', this.peer_id);
Object.keys(_EVENTS).forEach((evt) => {
this.eventListeners.set(evt, []);
});
this.socket.request = function request(type, data = {}) {
return new Promise((resolve, reject) => {
socket.emit(type, data, (data) => {
if (data.error) {
reject(data.error);
} else {
resolve(data);
}
});
});
};
// ####################################################
// CREATE ROOM AND JOIN
// ####################################################
this.createRoom(this.room_id).then(async () => {
const data = {
room_id: this.room_id,
peer_info: this.peer_info,
};
await this.join(data);
this.initSockets();
this._isConnected = true;
successCallback();
});
}
// ####################################################
// GET STARTED
// ####################################################
async createRoom(room_id) {
await this.socket
.request('createRoom', {
room_id,
})
.catch((err) => {
console.log('Create room:', err);
});
}
async join(data) {
this.socket
.request('join', data)
.then(async (room) => {
console.log('##### JOIN ROOM #####', room);
if (room?.maxParticipantsReached) {
console.warn('00-WARNING ----> Room is full, maximum participants reached!');
endRoomSession();
return popupHtmlMessage(
null,
image.forbidden,
'Join Room',
`Room is full, maximum participants${room?.maxParticipants ? ` (${room.maxParticipants})` : ''} reached!`,
'center',
'/',
false
);
}
if (room === 'invalid') {
console.warn('00-WARNING ----> Invalid Room name! Path traversal pattern detected!');
return this.roomInvalid();
}
if (room === 'notAllowed') {
console.warn(
'00-WARNING ----> Room is Unauthorized for current user, please provide a valid room name for this user'
);
return this.userRoomNotAllowed();
}
if (room === 'unauthorized') {
console.warn(
'00-WARNING ----> Room is Unauthorized for current user, please provide a valid username and password'
);
return this.userUnauthorized();
}
if (room === 'isLocked') {
this.RoomIsLocked = true;
this.event(_EVENTS.roomLock);
console.warn('00-WARNING ----> Room is Locked, Try to unlock by the password');
return this.unlockTheRoom();
}
if (room === 'isLobby') {
this.RoomIsLobby = true;
this.event(_EVENTS.lobbyOn);
console.warn('00-WARNING ----> Room Lobby Enabled, Wait to confirm my join');
return this.waitJoinConfirm();
}
if (room === 'isBanned') {
console.warn('00-WARNING ----> You are Banned from the Room!');
return this.isBanned();
}
// ##########################################
this.peers = new Map(JSON.parse(room.peers));
// ##########################################
if (this.usernameExists(this.peers)) {
return this.userNameAlreadyInRoom();
}
await this.joinAllowed(room);
})
.catch((error) => {
console.error('Join error:', error);
//
popupHtmlMessage(null, image.network, 'Join Room', error, 'center', false, true);
});
}
usernameExists(peers) {
if (!peer_info.peer_token) {
// hack...
for (let peer of Array.from(peers.keys()).filter((id) => id !== this.peer_id)) {
let peer_info = peers.get(peer).peer_info;
if (peer_info.peer_name == this.peer_name) {
console.log('07.0-WARNING ----> Username already in use');
return true;
}
}
}
return false;
}
async joinAllowed(room) {
console.log('07 ----> Join Room allowed');
await this.handleRoomInfo(room);
await this.loadDeviceAndInitTransports();
// ###############################################
this.socket.emit('getProducers'); // newProducers
// ###############################################
if (isBroadcastingEnabled) {
isPresenter ? await this.startLocalMedia() : this.handleRoomBroadcasting();
} else {
await this.startLocalMedia();
}
}
async loadDeviceAndInitTransports() {
// Get Router Capabilities
const routerRtpCapabilities = await this.socket.request('getRouterRtpCapabilities');
routerRtpCapabilities.headerExtensions = routerRtpCapabilities.headerExtensions.filter(
(ext) => ext.uri !== 'urn:3gpp:video-orientation'
);
// Load device
this.device = await this.loadDevice(routerRtpCapabilities);
console.log('07.3 ----> Get Router Rtp Capabilities codecs: ', this.device.rtpCapabilities.codecs);
// Init Send/Receive Transports
await this.initTransports(this.device);
}
async handleRoomInfo(room) {
// ##########################################
this.peers = new Map(JSON.parse(room.peers));
// ##########################################
console.log('07.0 ----> Room Survey', room.survey);
survey = room.survey;
console.log('07.0 ----> Room Leave Redirect', room.redirect);
redirect = room.redirect;
participantsCount = this.peers.size;
// ME
for (let peer of Array.from(this.peers.keys()).filter((id) => id == this.peer_id)) {
let my_peer_info = this.peers.get(peer).peer_info;
console.log('07.1 ----> My Peer info', my_peer_info);
isPresenter = window.localStorage.isReconnected === 'true' ? isPresenter : my_peer_info.peer_presenter;
this.peer_info.peer_presenter = isPresenter;
this.getId('isUserPresenter').innerText = isPresenter;
window.localStorage.isReconnected = false;
// GLOBAL LOBBY ENABLED
if (room?.globalLobby) {
if (isPresenter) {
localStorageSettings.lobby = true;
lS.setSettings(localStorageSettings);
console.warn('7.1-WARNING ----> GLOBAL Room Lobby detected, save the config');
}
rc.roomAction('globalLobbyOn', true, false);
console.warn('7.1-WARNING ----> GLOBAL Room Lobby detected');
}
handleRules(isPresenter);
// ###################################################################################################
isBroadcastingEnabled = isPresenter && !room.broadcasting ? isBroadcastingEnabled : room.broadcasting;
console.log('07.1 ----> ROOM BROADCASTING', isBroadcastingEnabled);
// ###################################################################################################
if (BUTTONS.settings.tabRecording) {
room.config.hostOnlyRecording
? (console.log('07.1 ----> WARNING Room Host only recording enabled'),
this.event(_EVENTS.hostOnlyRecordingOn))
: this.event(_EVENTS.hostOnlyRecordingOff);
}
// ###################################################################################################
if (room.recording) this.recording = room.recording;
if (room.recording && room.recording.recSyncServerRecording) {
console.log('07.1 WARNING ----> SERVER SYNC RECORDING ENABLED!');
this.recording.recSyncServerRecording = localStorageSettings.rec_server;
if (BUTTONS.settings.tabRecording && !room.config.hostOnlyRecording) {
show(roomRecordingServer);
}
switchServerRecording.checked = this.recording.recSyncServerRecording;
}
console.log('07.1 ----> SERVER SYNC RECORDING', this.recording);
// ###################################################################################################
// Handle Room moderator rules
if (room.moderator && (!isRulesActive || !isPresenter)) {
console.log('07.2 ----> ROOM MODERATOR', room.moderator);
// Update `this._moderator` with properties from `room.moderator`, keeping existing ones.
this._moderator = { ...this._moderator, ...room.moderator };
if (this._moderator.video_start_privacy || localStorageSettings.moderator_video_start_privacy) {
this.peer_info.peer_video_privacy = true;
this.emitCmd({
type: 'privacy',
peer_id: this.peer_id,
active: true,
broadcast: true,
});
this.userLog('warning', 'The Moderator starts your video in privacy mode', 'top-end');
}
if (this._moderator.audio_start_muted && this._moderator.video_start_hidden) {
this.userLog('warning', 'The Moderator disabled your audio and video', 'top-end');
} else {
if (this._moderator.audio_start_muted && !this._moderator.video_start_hidden) {
this.userLog('warning', 'The Moderator disabled your audio', 'top-end');
}
if (!this._moderator.audio_start_muted && this._moderator.video_start_hidden) {
this.userLog('warning', 'The Moderator disabled your video', 'top-end');
}
}
//
this._moderator.audio_cant_unmute ? hide(tabAudioDevicesBtn) : show(tabAudioDevicesBtn);
this._moderator.video_cant_unhide ? hide(tabVideoDevicesBtn) : show(tabVideoDevicesBtn);
}
// Check if VideoAI is enabled
if (!room.videoAIEnabled) {
VideoAI.enabled = false;
elemDisplay('tabVideoAIBtn', false);
}
// Check che RTMP config
if (room.rtmp) {
console.log('RTMP config', room.rtmp);
const { enabled, fromFile, fromUrl, fromStream } = room.rtmp;
elemDisplay('tabRTMPStreamingBtn', enabled);
elemDisplay('rtmpFromFile', fromFile);
elemDisplay('rtmpFromUrl', fromUrl);
elemDisplay('rtmpFromStream', fromStream);
if (!fromFile && !fromUrl && !fromStream) {
elemDisplay('tabRTMPStreamingBtn', false);
}
}
// There is polls
if (room.thereIsPolls) {
this.socket.emit('updatePoll');
}
// Host protected enabled in the server side
if (room.hostProtected) {
RoomURL = window.location.origin + '/join/' + room_id;
}
// Share Media Data on Join
if (
room.shareMediaData &&
Object.keys(room.shareMediaData).length !== 0 &&
room.shareMediaData.action === 'open'
) {
this.shareVideoAction(room.shareMediaData);
}
// Dominant Speaker
this.dominantSpeaker = room.dominantSpeaker || false;
if (!this.dominantSpeaker) {
elemDisplay('dominantSpeakerFocusDiv', false);
}
// Open Chat on Join
if (chat) {
const chatButton = getId('chatButton');
if (chatButton) {
chatButton.click();
}
}
}
// PARTICIPANTS
for (let peer of Array.from(this.peers.keys()).filter((id) => id !== this.peer_id)) {
let peer_info = this.peers.get(peer).peer_info;
// console.log('07.1 ----> Remote Peer info', peer_info);
const { peer_id, peer_name, peer_avatar, peer_presenter, peer_video, peer_recording, peer_lobby } =
peer_info;
if (peer_lobby) {
this.lobbyAddPear({ peer_id, peer_avatar, peer_name });
continue;
}
const canSetVideoOff = !isBroadcastingEnabled || (isBroadcastingEnabled && peer_presenter);
if (!peer_video && canSetVideoOff) {
console.log('Detected peer video off ' + peer_name);
this.setVideoOff(peer_info, true);
}
if (peer_recording) {
this.handleRecordingAction({
peer_id: peer_id,
peer_name: peer_name,
peer_avatar: peer_avatar,
action: enums.recording.started,
});
}
}
this.refreshParticipantsCount();
console.log('07.2 Participants Count ---->', participantsCount);
if (BUTTONS.popup.shareRoomPopup && notify && participantsCount == 1) {
shareRoom();
} else {
if (this.isScreenAllowed) {
this.shareScreen();
}
sound('joined');
}
}
async loadDevice(routerRtpCapabilities) {
if (!routerRtpCapabilities) {
console.error('Router RTP Capabilities are required to load the device.');
this.userLog('error', 'Router RTP Capabilities are missing.', 'center', 6000);
return null;
}
let device;
try {
device = await this.mediasoupClient.Device.factory();
// device = await this.mediasoupClient.Device.factory({ handlerName: 'Safari12' }); // for testing only
console.log('Device created successfully:', device.handlerName);
} catch (error) {
if (error.name === 'UnsupportedError') {
console.error('Browser not supported:', error);
this.userLog('error', 'Browser not supported. Please try a different browser.', 'center', 6000);
} else {
console.error('Error creating device:', error);
this.userLog('error', `Failed to create device: ${error.message}`, 'center', 6000);
}
return null;
}
try {
await device.load({
routerRtpCapabilities,
preferLocalCodecsOrder: !!this.preferLocalCodecsOrder,
});
console.log(
`Device loaded successfully with router RTP capabilities (preferLocalCodecsOrder: ${!!this.preferLocalCodecsOrder})`,
device.rtpCapabilities
);
} catch (error) {
console.error('Error loading device with router RTP capabilities:', error);
this.userLog('error', `Failed to load device: ${error.message}`, 'center', 6000);
return null;
}
return device;
}
// ####################################################
// TRANSPORTS
// ####################################################
async initTransports(device) {
await this.initProducerTransport(device);
await this.initConsumerTransport(device);
}
// ####################################################
// PRODUCER TRANSPORT
// ####################################################
async initProducerTransport(device) {
const producerTransportData = await this.socket.request('createWebRtcTransport', {
forceTcp: false,
rtpCapabilities: device.rtpCapabilities,
});
if (producerTransportData.error) {
console.error('Producer Transport creation failed', producerTransportData.error);
return;
}
this.producerTransport = device.createSendTransport(producerTransportData);
this.setupProducerTransportHandlers();
}
setupProducerTransportHandlers() {
this.producerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await this.socket.request('connectTransport', {
transport_id: this.producerTransport.id,
dtlsParameters,
});
callback();
} catch (err) {
console.error('Producer Transport connection error', err);
errback(err);
}
});
this.producerTransport.on('produce', async ({ kind, appData, rtpParameters }, callback, errback) => {
try {
const { producer_id } = await this.socket.request('produce', {
producerTransportId: this.producerTransport.id,
kind,
appData,
rtpParameters,
});
callback({ id: producer_id });
} catch (err) {
errback(err);
}
});
this.producerTransport.on('connectionstatechange', async (state) => {
console.log(`Producer Transport state changed to: ${state}`, { id: this.producerTransport.id });
switch (state) {
case 'connecting':
console.log('Producer Transport connecting...');
break;
case 'connected':
console.log('✅ Producer Transport connected', { id: this.producerTransport.id });
break;
case 'disconnected':
console.warn('⚠️ Producer Transport disconnected', { id: this.producerTransport.id });
console.warn('⚠️ Producer Attempting ICE restart...');
try {
await this.restartProducerIce();
} catch (error) {
console.error('❌ Producer ICE restart failed', error.message);
}
break;
case 'failed':
console.warn('❌ Producer Transport failed', { id: this.producerTransport.id });
break;
default:
console.log('Producer transport connection state changed', {
state,
id: this.producerTransport.id,
});
break;
}
});
this.producerTransport.on('icegatheringstatechange', (state) => {
const normalStates = new Set(['new', 'gathering', 'complete']);
normalStates.has(state)
? console.log('Producer ICE gathering state', { state, id: this.producerTransport.id })
: console.warn('Unexpected Producer ICE gathering state', { state, id: this.producerTransport.id });
});
this.producerTransport.on('icecandidateerror', (error) => {
console.error('❌ Producer ICE candidate error', {
error: error,
id: this.producerTransport.id,
});
});
}
// ####################################################
// CONSUMER TRANSPORT
// ####################################################
async initConsumerTransport(device) {
const consumerTransportData = await this.socket.request('createWebRtcTransport', {
forceTcp: false,
});
if (consumerTransportData.error) {
console.error('Consumer Transport creation failed', consumerTransportData.error);
return;
}
this.consumerTransport = device.createRecvTransport(consumerTransportData);
this.setupConsumerTransportHandlers();
}
setupConsumerTransportHandlers() {
this.consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await this.socket.request('connectTransport', {
transport_id: this.consumerTransport.id,
dtlsParameters,
});
callback();
} catch (err) {
console.error('Consumer Transport connection error', err);
errback(err);
}
});
this.consumerTransport.on('connectionstatechange', async (state) => {
console.log(`Consumer Transport state changed to: ${state}`, { id: this.consumerTransport.id });
switch (state) {
case 'connecting':
console.log('Consumer Transport connecting...');
break;
case 'connected':
console.log('✅ Consumer Transport connected', { id: this.consumerTransport.id });
break;
case 'disconnected':
console.warn('⚠️ Consumer Transport disconnected', { id: this.consumerTransport.id });
console.warn('⚠️ Consumer Attempting ICE restart...');
try {
await this.restartConsumerIce();
} catch (error) {
console.error('❌ Consumer ICE restart failed', error.message);
}
break;
case 'failed':
console.warn('❌ Consumer Transport failed', { id: this.consumerTransport.id });
break;
default:
console.log('Consumer transport connection state changed', {
state,
id: this.consumerTransport.id,
});
break;
}
});
this.consumerTransport.on('icegatheringstatechange', (state) => {
const normalStates = new Set(['new', 'gathering', 'complete']);
normalStates.has(state)
? console.log('Consumer ICE gathering state', { state, id: this.consumerTransport.id })
: console.warn('Unexpected Consumer ICE gathering state', { state, id: this.consumerTransport.id });
});
this.consumerTransport.on('icecandidateerror', (error) => {
console.error('❌ Consumer ICE candidate error', {
error: error,
id: this.consumerTransport.id,
});
});
}
// ####################################################
// TODO: DATA TRANSPORT
// ####################################################
// ####################################################
// HANDLE ICE
// ####################################################
async restartTransportIce(transport, type) {
if (!transport || typeof transport !== 'object' || transport.closed) return false;
try {
console.warn(`🔄 ${type} Restarting ICE...`, {
id: transport.id,
state: transport.connectionState,
});
const iceParameters = await this.socket.request('restartIce', {
transport_id: transport.id,
});
if (!iceParameters) {
console.warn(`⚠️ No ${type} ICE Parameters received`);
return false;
}
console.info(`🚀 ${type} Restarting transport ICE`, iceParameters);
await transport.restartIce({ iceParameters });
console.info(`✅ Successfully restarted ${type} ICE`);
return true;
} catch (error) {
console.error(`🔥 ${type} Restart ICE error`, {
id: transport?.id,
error: error,
});
return false;
}
}
async restartTransportWithRetry(transport, transportType, maxRetries = 5, initialDelay = 1000) {
let delay = initialDelay;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const reconnected = await this.restartTransportIce(transport, transportType);
if (reconnected) {
console.info(`✅ ${transportType} reconnected successfully on attempt ${attempt}.`);
return true;
}
if (attempt < maxRetries) {
console.warn(`🌀 ${transportType} reconnection attempt ${attempt} failed. Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff: 1s -> 2s -> 4s -> 8s -> 16s
} else {
console.error(`❌ ${transportType} failed to reconnect after ${maxRetries} attempts.`);
}
}
console.error('❌ Failed to reconnect after multiple attempts.');
transport.close();
popupHtmlMessage(
null,
image.network,
`${transportType} Transport`,
'Unable to reconnect. Please check your network.',
'center',
false,
true
);
return false;
}
async restartProducerIce(retries = 5, delay = 1000) {
return this.restartTransportWithRetry(this.producerTransport, 'Producer', retries, delay);
}
async restartConsumerIce(retries = 5, delay = 1000) {
return this.restartTransportWithRetry(this.consumerTransport, 'Consumer', retries, delay);
}
async restartIce() {
if (this.iceRestarting) return;
console.warn('Restart ICE...', {
producerTransportConnectionState: this.producerTransport.connectionState,
consumerTransportConnectionState: this.consumerTransport.connectionState,
});
try {
this.iceRestarting = true;
await this.restartProducerIce();
await this.restartConsumerIce();
console.log('✅ Restart ICE done');
} catch (error) {
console.error('❌ Restart ICE error', error);
} finally {
this.iceRestarting = false;
}
}
// ####################################################
// SOCKET ON
// ####################################################
initSockets() {
this.socket.io.on('reconnect_attempt', this.handleSocketReconnectAttempt);
this.socket.io.on('reconnect', this.handleSocketReconnect);
this.socket.io.on('reconnect_failed', this.handleSocketReconnectFailed);
this.socket.on('connect', this.handleSocketConnect);
this.socket.on('connect_error', this.handleSocketConnectionError);
this.socket.on('disconnect', this.handleSocketDisconnect);
this.socket.on('consumerClosed', this.handleConsumerClosed);
this.socket.on('setVideoOff', this.handleSetVideoOff);
this.socket.on('removeMe', this.handleRemoveMe);
this.socket.on('refreshParticipantsCount', this.handleRefreshParticipantsCount);
this.socket.on('newProducers', this.handleNewProducers);
this.socket.on('message', this.handleMessage);
this.socket.on('roomAction', this.handleRoomAction);
this.socket.on('roomPassword', this.handleRoomPassword);
this.socket.on('roomLobby', this.handleRoomLobby);
this.socket.on('cmd', this.handleCmdData);
this.socket.on('peerAction', this.handlePeerAction);
this.socket.on('updatePeerInfo', this.handleUpdatePeerInfo);
this.socket.on('fileInfo', this.handleFileInfoData);
this.socket.on('file', this.handleFileData);
this.socket.on('shareVideoAction', this.handleShareVideoAction);
this.socket.on('fileAbort', this.handleFileAbortData);
this.socket.on('receiveFileAbort', this.handleReceiveFileAbortData);
this.socket.on('wbCanvasToJson', this.handleWbCanvasToJson);
this.socket.on('whiteboardAction', this.handleWhiteboardAction);
this.socket.on('audioVolume', this.handleAudioVolumeData);
this.socket.on('dominantSpeaker', this.handleDominantSpeakerData);
this.socket.on('updateRoomModerator', this.handleUpdateRoomModeratorData);
this.socket.on('updateRoomModeratorALL', this.handleUpdateRoomModeratorALLData);
this.socket.on('recordingAction', this.handleRecordingActionData);
this.socket.on('endRTMP', this.handleEndRTMP);
this.socket.on('errorRTMP', this.handleErrorRTMP);
this.socket.on('endRTMPfromURL', this.handleEndRTMPfromURL);
this.socket.on('errorRTMPfromURL', this.handleErrorRTMPfromURL);
this.socket.on('updatePolls', this.handleUpdatePolls);
this.socket.on('editorChange', this.handleEditorChange);
this.socket.on('editorActions', this.handleEditorActions);
this.socket.on('editorUpdate', this.handleEditorUpdate);
}
// ####################################################
// HANDLE SOCKET DATA
// ####################################################
handleSocketConnect = () => {
console.log('SocketOn Connected to signaling server!');
};
handleSocketDisconnect = (reason) => {
console.log(`SocketOn Disconnect Reason: ${reason}`);
this.handleDisconnect(reason);
};
handleSocketConnectionError = (err) => {
console.log(`SocketOn Disconnect Error: ${err.message}`);
};
handleSocketReconnectAttempt = (attempt) => {
console.log(`SocketOn Reconnect Attempt: ${attempt}`);
this.handleReconnectAttempt(attempt);
};
handleSocketReconnect = () => {
console.log('SocketOn Reconnected to signaling server!');
this.handleReconnect();
};
handleSocketReconnectFailed = () => {
console.error('SocketOn Reconnect failed');
this.handleReconnectFailed();
};
handleConsumerClosed = ({ consumer_id, consumer_kind }) => {
console.log('SocketOn Closing consumer', { consumer_id, consumer_kind });
this.removeConsumer(consumer_id, consumer_kind);
};
handleSetVideoOff = (data) => {
if (!isBroadcastingEnabled || (isBroadcastingEnabled && data.peer_presenter)) {
console.log('SocketOn setVideoOff', {
peer_name: data.peer_name,
peer_presenter: data.peer_presenter,
});
this.setVideoOff(data, true);
}
};
handleRemoveMe = (data) => {
console.log('SocketOn Remove me:', data);
this.removeVideoOff(data.peer_id);
this.lobbyRemoveMe(data.peer_id);
participantsCount = data.peer_counts;
if (!isBroadcastingEnabled) adaptAspectRatio(participantsCount);
if (isParticipantsListOpen) getRoomParticipants();
if (isBroadcastingEnabled && data.isPresenter) {
this.userLog('info', `${icons.broadcaster} ${data.peer_name} disconnected`, 'top-end', 6000);
}
};
handleRefreshParticipantsCount = (data) => {
console.log('SocketOn Participants Count:', data);
participantsCount = data.peer_counts;
if (isBroadcastingEnabled) {
if (isParticipantsListOpen) getRoomParticipants();
wbUpdate();
this.editorUpdate();
} else {
adaptAspectRatio(participantsCount);
}
};
handleNewProducers = async (data) => {
if (data.length > 0) {
console.log('SocketOn New producers', {
data,
password: {
roomIsLocked: this.RoomIsLocked,
roomPasswordValid: this.RoomPasswordValid,
},
lobby: {
roomIsLobby: this.RoomIsLobby,
roomLobbyAccepted: this.RoomLobbyAccepted,
},
});
if (this.RoomIsLocked && !this.RoomPasswordValid) {
console.log('Access denied: Room is locked and password has not been validated yet', data);
return;
}
if (this.RoomIsLobby && !this.RoomLobbyAccepted) {
console.log('Access pending: Lobby mode is active, waiting for approval to join', data);
return;
}
for (let { producer_id, peer_name, peer_info, type } of data) {
await this.consume(producer_id, peer_name, peer_info, type);
}
}
};
handleMessage = (data) => {
console.log('SocketOn New message:', data);
this.showMessage(data);
};
handleRoomAction = (data) => {
console.log('SocketOn Room action:', data);
this.roomAction(data, false);
};
handleRoomPassword = (data) => {
console.log('SocketOn Room password:', data.password);
this.roomPassword(data);
};
handleRoomLobby = (data) => {
console.log('SocketOn Room lobby:', data);
this.roomLobby(data);
};
handleCmdData = (data) => {
console.log('SocketOn Peer cmd:', data);
this.handleCmd(data);
};
handlePeerAction = (data) => {
console.log('SocketOn Peer action:', data);
this.peerAction(data.from_peer_name, data.peer_id, data.action, false, data.broadcast, true, data.message);
};
handleUpdatePeerInfo = (data) => {
console.log('SocketOn Peer info update:', data);
this.updatePeerInfo(data.peer_name, data.peer_id, data.type, data.status, false, data.peer_presenter);
};
handleFileInfoData = (data) => {
console.log('SocketOn File info:', data);
this.handleFileInfo(data);
};
handleFileData = (data) => {
this.handleFile(data);
};
handleShareVideoAction = (data) => {
this.shareVideoAction(data);
};
handleFileAbortData = (data) => {
this.handleFileAbort(data);
};
handleReceiveFileAbortData = (data) => {
this.handleReceiveFileAbort(data);
};
handleWbCanvasToJson = (data) => {
console.log('SocketOn Received whiteboard canvas JSON');
JsonToWbCanvas(data);
};
handleWhiteboardAction = (data) => {
console.log('Whiteboard action', data);
whiteboardAction(data, false);
};
handleAudioVolumeData = (data) => {
this.handleAudioVolume(data);
};
handleDominantSpeakerData = (data) => {
this.handleDominantSpeaker(data);
};
handleUpdateRoomModeratorData = (data) => {
console.log('SocketOn Update room moderator', data);
this.handleUpdateRoomModerator(data);
};
handleUpdateRoomModeratorALLData = (data) => {
console.log('SocketOn Update room moderator ALL', data);
this.handleUpdateRoomModeratorALL(data);
};
handleRecordingActionData = (data) => {
console.log('SocketOn Recording action:', data);
this.handleRecordingAction(data);
};
handleEndRTMP = (data) => {
this.endRTMP(data);
};
handleErrorRTMP = (data) => {
this.errorRTMP(data);
};
handleEndRTMPfromURL = (data) => {
this.endRTMPfromURL(data);
};
handleErrorRTMPfromURL = (data) => {
this.errorRTMPfromURL(data);
};
handleUpdatePolls = (data) => {
this.pollsUpdate(data);
};
handleEditorChange = (data) => {
this.handleEditorData(data);
};
handleEditorActions = (data) => {
this.handleEditorActionsData(data);
};
handleEditorUpdate = (data) => {
this.handleEditorUpdateData(data);
};
// ####################################################
// SOCKET RECONNECT/DISCONNECT
// ####################################################
showReconnectAlert(reason) {
if (this.silentReconnect) return;
this.reconnectAlert = Swal.fire({
background: swalBackground,
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: false,
showConfirmButton: false,
position: 'top',
icon: 'warning',
title: 'Lost connection',
text: `${reason}, trying to reconnect...`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
showMaxAttemptsAlert() {
if (this.silentReconnect) return;
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: false,
showConfirmButton: true,
background: swalBackground,
position: 'top',
icon: 'warning',
title: 'Unable to reconnect',
text: 'Please check your internet connection!',
icon: 'error',
confirmButtonText: 'Join Room',
confirmButtonColor: '#18392B',
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
this.refreshBrowser();
}
});
}
showServerAwayMessage() {
if (this.serverAwayShown) return;
this.serverAwayShown = true;
console.warn('Server away or in maintenance, please wait...');
this.ServerAway();
this.exit(true);
}
attemptReconnect(attempt) {
if (this._isConnected || attempt >= this.maxReconnectAttempts) return;
const delay = Math.min(this.reconnectInterval * attempt, this.maxReconnectInterval);
this.updateReconnectAlert(delay);
}
handleDisconnect(reason) {
endRoomSession();
window.localStorage.isReconnected = true;
console.log('Disconnected. Attempting to reconnect...');
// Immediately save recording if active
if (this.isRecording()) {
this.saveRecording('Socket disconnected');
}
this.serverAwayShown = false;
this._isConnected = false;
this.showReconnectAlert(reason);
}
handleReconnectAttempt(attempt) {
attempt < this.maxReconnectAttempts ? this.attemptReconnect(attempt) : this.showServerAwayMessage();
}
handleReconnect() {
this._isConnected = true;
this.closeReconnectAlert();
this.refreshBrowser();
}
handleReconnectFailed() {
if (!this._isConnected) {
this.closeReconnectAlert();
this.showMaxAttemptsAlert();
}
}
updateReconnectAlert(delay) {
if (this.reconnectAlert) {
this.reconnectAlert.update({
title: 'Reconnecting',
text: `Reconnection attempt in ${delay / 1000} seconds...`,
});
}
}
closeReconnectAlert() {
if (this.reconnectAlert) {
this.reconnectAlert.close();
}
}
// ####################################################
// SERVER AWAY/MAINTENANCE
// ####################################################
ServerAway() {
this.sound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: false,
showConfirmButton: false,
background: swalBackground,
position: 'top',
icon: 'warning',
title: 'Server away',
text: 'The server seems away or in maintenance, please wait until it come back up.',
denyButtonText: `Leave room`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (!result.isConfirmed) {
this.event(_EVENTS.exitRoom);
}
});
}
removePeerInfoFromLocalStorage() {
try {
localStorage.removeItem('sfu_peer_info');
} catch (e) {
console.warn('Unable to remove sfu_peer_info from localStorage:', e);
}
}
updatePeerInfoInLocalStorage() {
try {
localStorage.setItem('sfu_peer_info', JSON.stringify(this.peer_info));
} catch (e) {
console.warn('Unable to save peer_info to localStorage:', e);
}
}
getPeerInfoFromLocalStorage() {
try {
const sfu_peer_info = localStorage.getItem('sfu_peer_info');
return sfu_peer_info ? JSON.parse(sfu_peer_info) : null;
} catch (e) {
console.warn('Unable to get sfu_peer_info from localStorage:', e);
return null;
}
}
refreshBrowser() {
endRoomSession();
this.updatePeerInfoInLocalStorage();
const reconnectDirectJoinURL = this.getReconnectDirectJoinURL();
setTimeout(() => {
this.exit(true);
openURL(reconnectDirectJoinURL);
this.removePeerInfoFromLocalStorage();
}, 100);
}
getReconnectDirectJoinURL() {
const sfu_peer_info = this.getPeerInfoFromLocalStorage();
const { peer_presenter, peer_audio, peer_video, peer_screen, peer_token } = sfu_peer_info
? sfu_peer_info
: this.peer_info;
const baseUrl = `${window.location.origin}/join`;
const queryParams = {
room: this.room_id,
roomPassword: this.RoomPassword,
name: this.peer_name,
audio: peer_audio,
video: peer_video,
screen: peer_screen,
notify: 0,
isPresenter: peer_presenter || isPresenter,
};
if (peer_token) queryParams.token = peer_token;
const url = `${baseUrl}?${Object.entries(queryParams)
.map(([key, value]) => `${key}=${value}`)
.join('&')}`;
return url;
}
// ####################################################
// CHECK USER
// ####################################################
userNameAlreadyInRoom() {
this.sound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
imageUrl: image.user,
position: 'center',
title: 'Username',
html: `The Username is already in use.
Please try with another one`,
showDenyButton: false,
confirmButtonText: `OK`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
endRoomSession();
openURL((window.location.href = '/join/' + this.room_id));
}
});
}
// ####################################################
// HANDLE ROOM BROADCASTING
// ####################################################
handleRoomBroadcasting() {
console.log('07.4 ----> Room Broadcasting is currently active, and you are not the designated presenter');
this.peer_info.peer_audio = false;
this.peer_info.peer_video = false;
this.peer_info.peer_screen = false;
const mediaTypes = ['audio', 'video', 'screen'];
mediaTypes.forEach((type) => {
const data = {
room_id: this.room_id,
peer_name: this.peer_name,
peer_id: this.peer_id,
peer_presenter: isPresenter,
type: type,
status: false,
broadcast: true,
};
this.socket.emit('updatePeerInfo', data);
});
handleRulesBroadcasting();
}
toggleRoomBroadcasting() {
Swal.fire({
background: swalBackground,
position: 'center',
imageUrl: image.broadcasting,
title: 'Room broadcasting Enabled',
text: 'Would you like to continue the room broadcast?',
showDenyButton: true,
confirmButtonColor: '#18392B',
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isDenied) {
switchBroadcasting.click();
}
});
}
// ####################################################
// START LOCAL AUDIO VIDEO MEDIA
// ####################################################
async startLocalMedia() {
console.log('08 ----> START LOCAL MEDIA...');
const audioProducerExist = this.producerExist(mediaType.audio);
if (this.isAudioAllowed) {
if (!audioProducerExist) {
await this.produce(mediaType.audio, microphoneSelect.value);
console.log('09 ----> START AUDIO MEDIA');
}
if (this._moderator.audio_start_muted) {
await this.sleep(300);
await this.pauseAudioProducer();
}
} else {
if (isEnumerateAudioDevices && !audioProducerExist) {
await this.produce(mediaType.audio, microphoneSelect.value);
console.log('09 ----> START AUDIO MEDIA');
await this.sleep(300);
await this.pauseAudioProducer();
}
}
if (this.isVideoAllowed && !this._moderator.video_start_hidden) {
await this.produce(mediaType.video, videoSelect.value);
console.log('10 ----> START VIDEO MEDIA');
} else {
setColor(startVideoButton, 'red');
this.setVideoOff(this.peer_info, false);
this.sendVideoOff();
if (BUTTONS.main.startVideoButton) this.event(_EVENTS.stopVideo);
this.updatePeerInfo(this.peer_name, this.peer_id, 'video', false);
console.log('10 ----> VIDEO IS OFF');
}
if (this.joinRoomWithScreen && !this._moderator.screen_cant_share) {
await this.produce(mediaType.screen, null, false, true);
console.log('11 ----> START SCREEN MEDIA');
}
console.log('[startLocalMedia] - PRODUCER LABEL', this.producerLabel);
}
async pauseAudioProducer() {
setColor(startAudioButton, 'red');
this.setIsAudio(this.peer_id, false);
if (BUTTONS.main.startAudioButton) this.event(_EVENTS.stopAudio);
await this.pauseProducer(mediaType.audio);
console.log('09 ----> PAUSE AUDIO MEDIA');
this.updatePeerInfo(this.peer_name, this.peer_id, 'audio', false);
}
// ####################################################
// PRODUCER
// ####################################################
async produce(type, deviceId = null, swapCamera = false, init = false) {
let mediaConstraints = {};
let elem;
let stream;
let audio = false;
let video = false;
let screen = false;
switch (type) {
case mediaType.audio:
this.isAudioAllowed = true;
mediaConstraints = this.getAudioConstraints(deviceId);
this.peer_info.peer_audio = true;
audio = true;
break;
case mediaType.video:
this.isVideoAllowed = true;
mediaConstraints = swapCamera ? this.getCameraConstraints() : this.getVideoConstraints(deviceId);
this.peer_info.peer_video = true;
video = true;
break;
case mediaType.screen:
mediaConstraints = this.getScreenConstraints();
this.peer_info.peer_screen = true;
screen = true;
break;
default:
return;
}
if (!this.device.canProduce('video') && !audio) {
return console.error('Cannot produce video');
}
if (this.producerLabel.has(type)) {
return console.warn('Producer already exists for this type ' + type);
}
const videoPrivacyBtn = this.getId(this.peer_id + '__vp');
if (videoPrivacyBtn) videoPrivacyBtn.style.display = screen ? 'none' : 'inline';
console.log(`Media constraints ${type}:`, mediaConstraints);
try {
if (init) {
stream = initStream;
} else {
stream = screen
? await navigator.mediaDevices.getDisplayMedia(mediaConstraints)
: await navigator.mediaDevices.getUserMedia(mediaConstraints);
// Handle Virtual Background and Blur using MediaPipe
if (video && isMediaStreamTrackAndTransformerSupported) {
const videoTrack = stream.getVideoTracks()[0];
if (virtualBackgroundBlurLevel) {
// Apply blur before sending it to WebRTC stream
stream = await virtualBackground.applyBlurToWebRTCStream(
videoTrack,
virtualBackgroundBlurLevel
);
} else if (virtualBackgroundSelectedImage) {
// Apply virtual background to WebRTC stream
stream = await virtualBackground.applyVirtualBackgroundToWebRTCStream(
videoTrack,
virtualBackgroundSelectedImage
);
} else if (virtualBackgroundTransparent) {
// Apply Transparent virtual background to WebRTC stream
stream = await virtualBackground.applyTransparentVirtualBackgroundToWebRTCStream(videoTrack);
}
}
}
if (audio && BUTTONS.settings.customNoiseSuppression) {
/*
* Initialize RNNoise Suppression if enabled and supported
* This will only apply to audio tracks
* and will not affect video tracks.
*/
this.initRNNoiseSuppression();
stream = await this.getRNNoiseSuppressionStream(stream);
}
console.log('Supported Constraints', navigator.mediaDevices.getSupportedConstraints());
const track = audio ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0];
if (screen) {
/*
* track.contentHint helps optimize media tracks for specific use cases:
* - 'motion': For high frame rate (video playback, game streaming)
* - 'detail': For high fidelity (screen sharing with text/graphics)
*/
if ('contentHint' in track) {
show(ScreenOptimizationDiv);
const contentHint = screenOptimization.value;
if (contentHint !== 'None') {
track.contentHint = contentHint;
console.info(`Optimized video track for screen sharing: ${contentHint}`);
}
} else {
hide(ScreenOptimizationDiv);
console.warn('contentHint is not supported in this browser');
}
}
console.log(`${type} settings ->`, track.getSettings());
const params = {
track,
headerExtensionOptions: {
absCaptureTime: true,
},
appData: {
mediaType: type,
},
};
if (audio) {
console.log('AUDIO ENABLE OPUS');
params.codecOptions = {
opusStereo: true,
opusDtx: true,
opusFec: true,
opusNack: true,
};
}
if (video) {
const { encodings, codec } = this.getWebCamEncoding();
console.log('GET WEBCAM ENCODING', {
encodings: encodings,
codecs: codec,
});
params.encodings = encodings;
params.codecs = codec;
params.codecOptions = {
videoGoogleStartBitrate: 1000,
};
}
if (screen) {
const { encodings, codec } = this.getScreenEncoding();
console.log('GET SCREEN ENCODING', {
encodings: encodings,
codecs: codec,
});
params.encodings = encodings;
params.codecs = codec;
params.codecOptions = {
videoGoogleStartBitrate: 1000,
};
}
console.log('PRODUCER TYPE AND PARAMS', {
type: type,
params: params,
});
const producer = await this.producerTransport.produce(params);
if (!producer) {
throw new Error('Producer not found!');
}
console.log('PRODUCER MEDIA TYPE ----> ' + type);
console.log('PRODUCER', producer);
this.producers.set(producer.id, producer);
this.producerLabel.set(type, producer.id);
// if screen sharing produce the tab audio + microphone
if (screen && stream.getAudioTracks()[0]) {
await this.produceScreenAudio(stream);
}
if (!audio) {
this.localVideoStream = stream;
elem = await this.handleProducer(producer.id, type, stream);
if (video) {
this.localVideoElement = elem;
this.videoProducerId = producer.id;
camera = detectCameraFacingMode(stream);
handleCameraMirror(elem);
}
if (screen) {
this.screenProducerId = producer.id;
if (elem.classList.contains('mirror')) {
elem.classList.remove('mirror');
}
}
} else {
this.localAudioStream = stream;
elem = await this.handleProducer(producer.id, type, stream);
this.audioProducerId = producer.id;
getMicrophoneVolumeIndicator(stream);
}
if (video) {
this.handleHideMe();
}
producer.on('trackended', () => {
this.closeProducer(type, 'trackended');
});
producer.on('transportclose', () => {
this.closeProducer(type, 'transportclose');
});
producer.on('close', () => {
this.closeProducer(type, 'close');
});
switch (type) {
case mediaType.audio:
this.setIsAudio(this.peer_id, true);
this.event(_EVENTS.startAudio);
break;
case mediaType.video:
this.setIsVideo(true);
this.event(_EVENTS.startVideo);
break;
case mediaType.screen:
this.setIsScreen(true);
this.event(_EVENTS.startScreen);
break;
default:
break;
}
this.sound('joined');
return producer;
} catch (err) {
console.error('Produce error:', err);
handleMediaError(type, err);
}
}
// ####################################################
// HANDLE VIRTUAL BACKGROUND AND BLUR
// ####################################################
showVideoImageSelector() {
const imageGrid = document.getElementById('imageGrid');
const imageGridVideo = document.getElementById('imageGridVideo');
elemDisplay('imageGridVideo', true, 'grid');
if (imageGridVideo.innerHTML != '') return;
imageGrid.innerHTML = ''; // Clear previous init images
imageGridVideo.innerHTML = ''; // Clear previous images
function createImage(id, src, tooltip, index, clickHandler) {
const img = document.createElement('img');
img.id = id;
img.src = src;
img.dataset.index = index;
img.addEventListener('click', clickHandler);
imageGridVideo.appendChild(img);
if (tooltip) {
setTippy(img.id, tooltip, 'top');
}
}
// Common function to handle virtual background changes
async function handleVirtualBackground(blurLevel = null, imgSrc = null, transparentBg = null) {
if (!blurLevel && !imgSrc && !transparentBg) {
virtualBackgroundBlurLevel = null;
virtualBackgroundSelectedImage = null;
virtualBackgroundTransparent = null;
}
await rc.applyVirtualBackground(blurLevel, imgSrc, transparentBg);
}
// Create clean virtual bg Image
createImage('cleanVbImg', image.user, 'Remove virtual background', 'cleanVb', () =>
handleVirtualBackground(null, null)
);
// Create High Blur Image
createImage('highBlurImg', image.blurHigh, 'High Blur', 'high', () => handleVirtualBackground(20));
// Create Low Blur Image
createImage('lowBlurImg', image.blurLow, 'Low Blur', 'low', () => handleVirtualBackground(10));
// Create transparent virtual bg Image
createImage('transparentBg', image.transparentBg, 'Transparent Virtual background', 'transparentVb', () =>
handleVirtualBackground(null, null, true)
);
// Handle file upload (common logic for file selection)
function setupFileUploadButton(buttonId, sourceImg, tooltip, handler) {
const imgButton = document.createElement('img');
imgButton.id = buttonId;
imgButton.src = sourceImg;
imgButton.addEventListener('click', handler);
imageGridVideo.appendChild(imgButton);
setTippy(imgButton.id, tooltip, 'top');
}
function handleFileUpload(file) {
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = async (e) => {
const imgData = e.target.result;
await indexedDBHelper.saveImage(imgData);
addImageToUI(imgData);
};
reader.readAsDataURL(file);
}
}
function createUploadImageButton() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
fileInput.addEventListener('change', (event) => {
handleFileUpload(event.target.files[0]);
});
setupFileUploadButton('uploadImg', image.upload, 'Upload your custom image', () => fileInput.click());
return fileInput;
}
// Function to add an image to UI
function addImageToUI(imgData) {
const imageContainer = document.createElement('div');
imageContainer.className = 'image-wrapper';
const customImg = document.createElement('img');
customImg.src = imgData;
customImg.addEventListener('click', () => handleVirtualBackground(null, imgData));
const deleteBtn = document.createElement('span');
deleteBtn.className = 'delete-icon fas fa-times';
deleteBtn.addEventListener('click', async (event) => {
event.stopPropagation();
await indexedDBHelper.removeImage(imgData);
imageContainer.remove();
});
imageContainer.appendChild(customImg);
imageContainer.appendChild(deleteBtn);
imageGridVideo.appendChild(imageContainer);
}
// Function to fetch and store an image from URL
async function fetchAndStoreImage(url) {
try {
const response = await fetch(url);
const blob = await response.blob();
const reader = new FileReader();
reader.onload = async (e) => {
const imgData = e.target.result;
await indexedDBHelper.saveImage(imgData);
addImageToUI(imgData);
};
reader.readAsDataURL(blob);
} catch (error) {
console.error('Error fetching image:', error);
// Detect CORS issue and provide a clearer error message
error.message.includes('Failed to fetch')
? showError(errorMessage, 'Error: Unable to fetch image. CORS policy may be blocking the request.')
: showError(errorMessage, `Error fetching image: ${error.message}`);
}
}
// Paste image from URL
function askForImageURL() {
elemDisplay(imageUrlModal.id, true);
navigator.clipboard
.readText()
.then((clipboardText) => {
if (isValidImageURL(filterXSS(clipboardText))) {
imageUrlInput.value = clipboardText;
}
})
.catch(() => {});
}
saveImageUrlBtn.addEventListener('click', async () => {
elemDisplay(imageUrlModal.id, false);
if (isValidImageURL(imageUrlInput.value)) {
await fetchAndStoreImage(imageUrlInput.value);
imageUrlInput.value = '';
}
});
cancelImageUrlBtn.addEventListener('click', () => {
elemDisplay(imageUrlModal.id, false);
imageUrlInput.value = '';
});
// Upload from file button
createUploadImageButton();
// Upload from URL button
setupFileUploadButton('linkImage', image.link, 'Upload Image from URL', askForImageURL);
// Load default virtual backgrounds
virtualBackgrounds.forEach((imageUrl, index) => {
createImage(`virtualBg${index}`, imageUrl, null, index + 1, () => handleVirtualBackground(null, imageUrl));
});
// Load stored images and add to image grid UI
indexedDBHelper.getAllImages().then((images) => images.forEach(addImageToUI));
// Upload image with drag and drop
imageGridVideo.addEventListener('dragover', (event) => {
event.preventDefault();
imageGridVideo.classList.add('drag-over');
});
imageGridVideo.addEventListener('dragleave', () => {
imageGridVideo.classList.remove('drag-over');
});
imageGridVideo.addEventListener('drop', (event) => {
event.preventDefault();
imageGridVideo.classList.remove('drag-over');
if (event.dataTransfer.files.length > 0) {
handleFileUpload(event.dataTransfer.files[0]);
}
});
}
// ####################################################
// VIRTUAL BACKGROUND HELPER
// ####################################################
async applyVirtualBackground(blurLevel, backgroundImage, backgroundTransparent) {
if (blurLevel) {
virtualBackgroundBlurLevel = blurLevel;
virtualBackgroundSelectedImage = null;
virtualBackgroundTransparent = null;
} else if (backgroundImage) {
virtualBackgroundBlurLevel = null;
virtualBackgroundSelectedImage = backgroundImage;
virtualBackgroundTransparent = null;
} else if (backgroundTransparent) {
virtualBackgroundBlurLevel = null;
virtualBackgroundSelectedImage = null;
virtualBackgroundTransparent = true;
} else {
virtualBackgroundBlurLevel = null;
virtualBackgroundSelectedImage = null;
virtualBackgroundTransparent = null;
}
videoSelect.onchange();
saveVirtualBackgroundSettings(blurLevel, backgroundImage, backgroundTransparent);
}
// ####################################################
// NOISE SUPPRESSION
// ####################################################
initRNNoiseSuppression() {
if (typeof RNNoiseProcessor === 'undefined') {
console.warn('RNNoiseProcessor is not available.');
return;
}
this.disableRNNoiseSuppression();
this.RNNoiseProcessor = new RNNoiseProcessor();
}
async getRNNoiseSuppressionStream(stream) {
if (!this.RNNoiseProcessor) {
console.warn('RNNoiseProcessor not initialized.');
return stream;
}
try {
const processedStream = await this.RNNoiseProcessor.startProcessing(stream);
if (localStorageSettings.mic_noise_suppression) {
this.RNNoiseProcessor.toggleNoiseSuppression();
switchNoiseSuppression.checked = this.RNNoiseProcessor.noiseSuppressionEnabled;
}
if (typeof labelNoiseSuppression !== 'undefined') {
labelNoiseSuppression.style.color = this.RNNoiseProcessor.noiseSuppressionEnabled ? 'lime' : 'white';
}
return processedStream;
} catch (err) {
console.warn('RNNoiseProcessor failed, using original stream:', err);
return stream;
}
}
disableRNNoiseSuppression() {
if (this.RNNoiseProcessor) {
this.RNNoiseProcessor.stopProcessing();
this.RNNoiseProcessor = null;
console.warn('RNNoiseProcessor already initialized, stopping previous instance.');
}
}
// ####################################################
// AUDIO/VIDEO/SCREEN CONSTRAINTS
// ####################################################
getAudioConstraints(deviceId) {
const audioConstraints = {
echoCancellation: true,
autoGainControl: true,
noiseSuppression: false,
sampleRate: 48000,
channelCount: 1,
};
if (deviceId) audioConstraints.deviceId = { exact: deviceId };
return {
audio: audioConstraints,
};
}
getCameraConstraints() {
camera = camera == 'user' ? 'environment' : 'user';
if (camera != 'user') this.camVideo = { facingMode: { exact: camera } };
else this.camVideo = true;
return {
audio: false,
video: this.camVideo,
};
}
getVideoConstraints(deviceId) {
const defaultFrameRate = { ideal: 30 };
const selectedValue = this.getSelectedIndexValue(videoFps);
const customFrameRate = parseInt(selectedValue, 10);
const frameRate = selectedValue === 'max' ? defaultFrameRate : customFrameRate;
// Helper to create constraints
function createConstraints(width, height, frameRate, isIdeal = false) {
const constraints = {
width: isIdeal ? { ideal: width } : { exact: width },
height: isIdeal ? { ideal: height } : { exact: height },
};
// Only add frameRate for non-Firefox browsers
if (!isFirefox) {
constraints.frameRate = isIdeal ? { ideal: frameRate } : frameRate;
}
return constraints;
}
let constraints = {};
switch (videoQuality.value) {
case 'default':
constraints = createConstraints(1280, 720, 30, true);
videoFps.selectedIndex = 0;
videoFps.disabled = true;
break;
case 'qvga':
constraints = createConstraints(320, 240, frameRate, isFirefox);
break;
case 'vga':
constraints = createConstraints(640, 480, frameRate, isFirefox);
break;
case 'hd':
constraints = createConstraints(1280, 720, frameRate, isFirefox);
break;
case 'fhd':
constraints = createConstraints(1920, 1080, frameRate, isFirefox);
break;
case '2k':
constraints = createConstraints(2560, 1440, frameRate, isFirefox);
break;
case '4k':
constraints = createConstraints(3840, 2160, frameRate, isFirefox);
break;
case '6k':
constraints = createConstraints(6144, 3456, frameRate, isFirefox);
break;
case '8k':
constraints = createConstraints(7680, 4320, frameRate, isFirefox);
break;
default:
// fallback to HD
constraints = createConstraints(1280, 720, frameRate, isFirefox);
break;
}
// Add deviceId if provided
if (deviceId) {
constraints.deviceId = { exact: deviceId };
}
// Compose final constraints object
return {
audio: false,
video: constraints,
};
}
getScreenConstraints() {
const defaultFrameRate = { ideal: 30 };
const selectedValue = this.getSelectedIndexValue(screenFps);
const customFrameRate = parseInt(selectedValue, 10);
const frameRate = selectedValue === 'max' ? defaultFrameRate : { ideal: customFrameRate };
// Base constraints structure with dynamic values for resolution and frame rate
const screenBaseConstraints = (width, height) => {
const videoConstraints = {
width: { ideal: width },
height: { ideal: height },
aspectRatio: 1.777, // 16:9 aspect ratio
};
if (!isFirefox) {
videoConstraints.frameRate = frameRate;
}
return {
audio: true,
video: videoConstraints,
};
};
const screenResolutionMap = {
hd: { width: 1280, height: 720 },
fhd: { width: 1920, height: 1080 },
'2k': { width: 2560, height: 1440 },
'4k': { width: 3840, height: 2160 },
'6k': { width: 6144, height: 3456 },
'8k': { width: 7680, height: 4320 },
};
// Default to Full HD if no match found in the screen resolution map
const { width, height } = screenResolutionMap[screenQuality.value] || { width: 1920, height: 1080 };
return screenBaseConstraints(width, height);
}
// ####################################################
// WEBCAM ENCODING
// ####################################################
getWebCamEncoding() {
let encodings;
let codec;
console.log('WEBCAM ENCODING', {
forceVP8: this.forceVP8,
forceVP9: this.forceVP9,
forceH264: this.forceH264,
forceAV1: this.forceAV1,
numSimulcastStreamsWebcam: this.numSimulcastStreamsWebcam,
enableWebcamLayers: this.enableWebcamLayers,
webcamScalabilityMode: this.webcamScalabilityMode,
rtpCapabilitiesCodecs: this.device.rtpCapabilities.codecs,
});
if (this.forceVP8) {
codec = this.device.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/vp8');
if (!codec) throw new Error('Desired VP8 codec+configuration is not supported');
} else if (this.forceH264) {
codec = this.device.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/h264');
if (!codec) throw new Error('Desired H264 codec+configuration is not supported');
} else if (this.forceVP9) {
codec = this.device.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/vp9');
if (!codec) throw new Error('Desired VP9 codec+configuration is not supported');
} else if (this.forceAV1) {
codec = this.device.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/av1');
if (!codec) throw new Error('Desired AV1 codec+configuration is not supported');
}
if (this.enableWebcamLayers) {
console.log('WEBCAM SIMULCAST/SVC ENABLED');
const firstVideoCodec = this.device.rtpCapabilities.codecs.find((c) => c.kind === 'video');
console.log('WEBCAM ENCODING: first codec available', { firstVideoCodec: firstVideoCodec });
// If VP9 is the only available video codec then use SVC.
if (
((this.forceVP9 || this.forceAV1) && codec) ||
(firstVideoCodec?.mimeType &&
['video/vp9', 'video/av1'].includes(firstVideoCodec.mimeType.toLowerCase()))
) {
console.log('WEBCAM ENCODING: VP9 or AV1 with SVC');
encodings = [
{
maxBitrate: 5000000,
scalabilityMode: this.webcamScalabilityMode || 'L3T3_KEY',
},
];
} else {
console.log('WEBCAM ENCODING: VP8 or H264 with simulcast');
encodings = [
{
scaleResolutionDownBy: 1,
maxBitrate: 5000000,
scalabilityMode: this.webcamScalabilityMode || 'L1T3',
},
];
if (this.numSimulcastStreamsWebcam > 1) {
encodings.unshift({
scaleResolutionDownBy: 2,
maxBitrate: 1000000,
scalabilityMode: this.webcamScalabilityMode || 'L1T3',
});
}
if (this.numSimulcastStreamsWebcam > 2) {
encodings.unshift({
scaleResolutionDownBy: 4,
maxBitrate: 500000,
scalabilityMode: this.webcamScalabilityMode || 'L1T3',
});
}
}
}
return { encodings, codec };
}
// ####################################################
// SCREEN ENCODING
// ####################################################
getScreenEncoding() {
let encodings;
let codec;
console.log('SCREEN ENCODING', {
forceVP8: this.forceVP8,
forceVP9: this.forceVP9,
forceH264: this.forceH264,
forceAV1: this.forceAV1,
numSimulcastStreamsSharing: this.numSimulcastStreamsSharing,
enableSharingLayers: this.enableSharingLayers,
sharingScalabilityMode: this.sharingScalabilityMode,
rtpCapabilitiesCodecs: this.device.rtpCapabilities.codecs,
});
if (this.forceVP8) {
codec = this.device.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/vp8');
if (!codec) throw new Error('Desired VP8 codec+configuration is not supported');
} else if (this.forceH264) {
codec = this.device.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/h264');
if (!codec) throw new Error('Desired H264 codec+configuration is not supported');
} else if (this.forceVP9) {
codec = this.device.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/vp9');
if (!codec) throw new Error('Desired VP9 codec+configuration is not supported');
} else if (this.forceAV1) {
codec = this.device.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/av1');
if (!codec) throw new Error('Desired AV1 codec+configuration is not supported');
}
if (this.enableSharingLayers) {
console.log('SCREEN SIMULCAST/SVC ENABLED');
const firstVideoCodec = this.device.rtpCapabilities.codecs.find((c) => c.kind === 'video');
console.log('SCREEN ENCODING: first codec available', { firstVideoCodec: firstVideoCodec });
// If VP9 is the only available video codec then use SVC.
if (
((this.forceVP9 || this.forceAV1) && codec) ||
(firstVideoCodec?.mimeType &&
['video/vp9', 'video/av1'].includes(firstVideoCodec.mimeType.toLowerCase()))
) {
console.log('SCREEN ENCODING: VP9 or AV1 with SVC');
encodings = [
{
maxBitrate: 5000000,
scalabilityMode: this.sharingScalabilityMode || 'L3T3',
dtx: true,
},
];
} else {
console.log('SCREEN ENCODING: VP8 or H264 with simulcast.');
encodings = [
{
scaleResolutionDownBy: 1,
maxBitrate: 5000000,
scalabilityMode: this.sharingScalabilityMode || 'L1T3',
dtx: true,
},
];
if (this.numSimulcastStreamsSharing > 1) {
encodings.unshift({
scaleResolutionDownBy: 2,
maxBitrate: 1000000,
scalabilityMode: this.sharingScalabilityMode || 'L1T3',
dtx: true,
});
}
if (this.numSimulcastStreamsSharing > 2) {
encodings.unshift({
scaleResolutionDownBy: 4,
maxBitrate: 500000,
scalabilityMode: this.sharingScalabilityMode || 'L1T3',
dtx: true,
});
}
}
}
return { encodings, codec };
}
// ####################################################
// HELPERS
// ####################################################
createButton(id, className) {
const button = document.createElement('button');
button.id = id;
button.className = className;
return button;
}
getConsumerIdByProducerId(producerId) {
for (let [consumerId, consumer] of this.consumers.entries()) {
if (consumer._producerId === producerId) {
return consumerId;
}
}
return null;
}
getProducerIdByConsumerId(consumerId) {
const consumer = this.consumers.get(consumerId);
if (consumer) {
return consumer._producerId;
}
return null;
}
// ####################################################
// PRODUCER
// ####################################################
handleHideMe() {
const myScreenWrap = this.getId(this.screenProducerId + '__video');
const myVideoWrap = this.getId(this.videoProducerId + '__video');
const myVideoWrapOff = this.getId(this.peer_id + '__videoOff');
const myVideoPinBtn = this.getId(this.videoProducerId + '__pin');
const myScreenPinBtn = this.getId(this.screenProducerId + '__pin');
console.log('handleHideMe', {
isHideMeActive: isHideMeActive,
myScreenWrap: myScreenWrap ? myScreenWrap.id : null,
myVideoWrap: myVideoWrap ? myVideoWrap.id : null,
myVideoWrapOff: myVideoWrapOff ? myVideoWrapOff.id : null,
myVideoPinBtn: myVideoPinBtn ? myVideoPinBtn.id : null,
myScreenPinBtn: myScreenPinBtn ? myScreenPinBtn.id : null,
});
if (myScreenWrap) myScreenWrap.style.display = isHideMeActive ? 'none' : 'block';
if (isHideMeActive && this.isVideoPinned && myVideoPinBtn) myVideoPinBtn.click();
if (isHideMeActive && this.isVideoPinned && myScreenPinBtn) myScreenPinBtn.click();
if (myVideoWrap) myVideoWrap.style.display = isHideMeActive ? 'none' : 'block';
if (myVideoWrapOff) myVideoWrapOff.style.display = isHideMeActive ? 'none' : 'block';
hideMeIcon.className = isHideMeActive ? html.hideMeOn : html.hideMeOff;
hideMeIcon.style.color = isHideMeActive ? 'red' : 'white';
isHideMeActive ? this.sound('left') : this.sound('joined');
resizeVideoMedia();
}
producerExist(type) {
return this.producerLabel.has(type);
}
closeThenProduce(type, deviceId = null, swapCamera = false) {
this.closeProducer(type, 'closeThenProduce');
setTimeout(async function () {
await rc.produce(type, deviceId, swapCamera);
}, 1000);
}
async handleProducer(id, type, stream) {
let elem, vb, vp, ts, d, p, i, au, pip, ha, fs, pm, pb, pn, pv, mv;
switch (type) {
case mediaType.video:
case mediaType.screen:
let isScreen = type === mediaType.screen;
this.removeVideoOff(this.peer_id);
d = document.createElement('div');
d.className = 'Camera';
d.id = id + '__video';
elem = document.createElement('video');
elem.setAttribute('id', id);
elem.setAttribute('volume', this.peer_id + '___pVolume');
!isScreen && elem.setAttribute('name', this.peer_id);
elem.setAttribute('playsinline', true);
elem.controls = isVideoControlsOn;
elem.autoplay = true;
elem.muted = true;
elem.volume = 0;
elem.poster = image.poster;
elem.style.objectFit = isScreen || isBroadcastingEnabled ? 'contain' : 'var(--videoObjFit)';
vb = document.createElement('div');
vb.id = id + '__vb';
vb.className = 'videoMenuBar hidden';
pip = this.createButton(id + '__pictureInPicture', html.pip);
ha = this.createButton(id + '__hideALL', html.hideALL + ' focusMode');
fs = this.createButton(id + '__fullScreen', html.fullScreen);
ts = this.createButton(id + '__snapshot', html.snapshot);
mv = this.createButton(id + '__mirror', html.mirror);
pn = this.createButton(id + '__pin', html.pin);
vp = this.createButton(this.peer_id + '__vp', html.videoPrivacy);
au = this.createButton(
this.peer_id + '__audio',
this.peer_info.peer_audio ? html.audioOn : html.audioOff
);
au.style.cursor = 'default';
p = document.createElement('p');
p.id = this.peer_id + '__name';
p.className = html.userName;
p.innerText = (isPresenter ? '⭐️ ' : '') + this.peer_name + ' (me)';
i = document.createElement('i');
i.id = this.peer_id + '__hand';
i.className = html.userHand;
pm = document.createElement('div');
pb = document.createElement('div');
pm.setAttribute('id', this.peer_id + '_pitchMeter');
pb.setAttribute('id', this.peer_id + '_pitchBar');
pm.className = 'speechbar';
pb.className = 'bar';
pb.style.height = '1%';
pm.appendChild(pb);
pv = document.createElement('input');
pv.id = this.peer_id + '___pVolume';
pv.type = 'range';
pv.min = 0;
pv.max = 100;
pv.value = 100;
BUTTONS.producerVideo.audioVolumeInput && vb.appendChild(pv);
BUTTONS.producerVideo.muteAudioButton && vb.appendChild(au);
BUTTONS.producerVideo.videoPrivacyButton && !isScreen && vb.appendChild(vp);
BUTTONS.producerVideo.snapShotButton && vb.appendChild(ts);
BUTTONS.producerVideo.videoPictureInPicture &&
this.isVideoPictureInPictureSupported &&
vb.appendChild(pip);
BUTTONS.producerVideo.videoMirrorButton && vb.appendChild(mv);
BUTTONS.producerVideo.fullScreenButton && this.isVideoFullScreenSupported && vb.appendChild(fs);
BUTTONS.producerVideo.focusVideoButton && vb.appendChild(ha);
if (!this.isMobileDevice) vb.appendChild(pn);
vb.appendChild(p);
d.appendChild(elem);
d.appendChild(pm);
d.appendChild(i);
d.appendChild(p);
const hideVideoMenu = () => {
if (vb && !vb.classList.contains('hidden')) {
hide(vb);
setCamerasBorderNone();
}
};
if (this.isMobileDevice) {
vb.classList.add('mobile-floating');
document.body.appendChild(vb);
} else {
vb.classList.remove('mobile-floating');
d.appendChild(vb);
d.addEventListener('mouseleave', hideVideoMenu);
}
vb.addEventListener('click', (e) => e.stopPropagation());
this.videoMediaContainer.appendChild(d);
await this.attachMediaStream(elem, stream, type, 'Producer');
this.myVideoEl = elem;
this.isVideoPictureInPictureSupported && this.handlePIP(elem.id, pip.id);
this.isVideoFullScreenSupported && this.handleFS(elem.id, fs.id);
this.handleVB(d.id, vb.id);
this.handleDD(elem.id, this.peer_id, true);
this.handleTS(elem.id, ts.id);
this.handleMV(elem.id, mv.id);
this.handleHA(ha.id, d.id);
this.handlePN(elem.id, pn.id, d.id, isScreen);
this.handleZV(elem.id, d.id, this.peer_id);
this.handlePV(id + '___' + pv.id);
this.setAV(
this.audioConsumers.get(this.peer_id + '___pVolume'),
this.peer_id + '___pVolume',
this.peer_info.peer_audio_volume
);
if (!isScreen) this.handleVP(elem.id, vp.id);
this.popupPeerInfo(p.id, this.peer_info);
this.checkPeerInfoStatus(this.peer_info);
if (isScreen && this.videoMediaContainer.childElementCount > 1) pn.click();
if (!this.isMobileDevice) {
this.setTippy(pn.id, 'Toggle Pin', 'bottom');
this.setTippy(ha.id, 'Toggle Focus mode', 'bottom');
this.setTippy(mv.id, 'Toggle mirror', 'bottom');
this.setTippy(pip.id, 'Toggle picture in picture', 'bottom');
this.setTippy(ts.id, 'Snapshot', 'bottom');
this.setTippy(vp.id, 'Toggle video privacy', 'bottom');
this.setTippy(au.id, 'Audio status', 'bottom');
}
handleAspectRatio();
console.log('[addProducer] Video-element-count', this.videoMediaContainer.childElementCount);
break;
case mediaType.audio:
elem = document.createElement('audio');
elem.setAttribute('id', id);
elem.setAttribute('name', 'LOCAL-AUDIO');
elem.setAttribute('volume', this.peer_id + '___pVolume');
elem.controls = false;
elem.autoplay = true;
elem.muted = true;
elem.volume = 0;
this.myAudioEl = elem;
this.localAudioEl.appendChild(elem);
await this.attachMediaStream(elem, stream, type, 'Producer');
const audioConsumerId = this.peer_id + '___pVolume';
this.audioConsumers.set(audioConsumerId, elem.id);
this.setAV(elem.id, audioConsumerId, this.peer_info.peer_audio_volume);
this.handlePV(elem.id + '___' + audioConsumerId);
console.log('[addProducer] audio-element-count', this.localAudioEl.childElementCount);
break;
default:
break;
}
return elem;
}
async pauseProducer(type) {
if (!this.producerLabel.has(type)) {
return console.warn('There is no producer for this type ' + type);
}
const producer_id = this.producerLabel.get(type);
this.producers.get(producer_id).pause();
try {
const response = await this.socket.request('pauseProducer', { producer_id, type });
console.log('Producer paused', response);
} catch (error) {
console.error('Error pausing producer', error);
}
switch (type) {
case mediaType.audio:
this.event(_EVENTS.pauseAudio);
break;
case mediaType.video:
this.event(_EVENTS.pauseVideo);
break;
case mediaType.screen:
this.event(_EVENTS.pauseScreen);
break;
default:
return;
}
}
async resumeProducer(type) {
if (!this.producerLabel.has(type)) {
return console.warn('There is no producer for this type ' + type);
}
const producer_id = this.producerLabel.get(type);
this.producers.get(producer_id).resume();
try {
const response = await this.socket.request('resumeProducer', { producer_id, type });
console.log('Producer resumed', response);
} catch (error) {
console.error('Error resuming producer', error);
}
switch (type) {
case mediaType.audio:
this.event(_EVENTS.resumeAudio);
break;
case mediaType.video:
this.event(_EVENTS.resumeVideo);
break;
case mediaType.screen:
this.event(_EVENTS.resumeScreen);
break;
default:
return;
}
}
closeProducer(type, event = 'Close Producer') {
if (!this.producerLabel.has(type)) {
return console.warn('There is no producer for this type ' + type);
}
const producer_id = this.producerLabel.get(type);
const producer = this.producers.get(producer_id);
// Stop all tracks of the producer's stream
if (producer && producer.track) {
try {
producer.track.stop();
} catch (err) {
console.warn('Error stopping producer track:', err);
}
}
const data = {
peer_name: this.peer_name,
producer_id: producer_id,
type: type,
status: false,
};
console.log(`${event} ${type}`, data);
this.socket.emit('producerClosed', data);
this.producers.get(producer_id).close();
this.producers.delete(producer_id);
this.producerLabel.delete(type);
console.log(`[${event}] - PRODUCER LABEL`, this.producerLabel);
if (type === mediaType.video || type === mediaType.screen) {
if (this.isVideoPinned && this.pinnedVideoPlayerId == producer_id) {
this.removeVideoPinMediaContainer();
console.log('Remove pin container due the Producer close', {
producer_id: producer_id,
producer_type: type,
});
}
const video = this.getId(producer_id);
this.removeVideoProducer(video, event);
}
if (type === mediaType.audio) {
const audio = this.getId(producer_id);
this.removeAudioProducer(audio, event);
}
if (type === mediaType.audioTab) {
const auTab = this.getId(producer_id);
this.removeAudioProducer(auTab, event);
}
switch (type) {
case mediaType.audioTab:
console.log('Closed audio tab');
break;
case mediaType.audio:
this.setIsAudio(this.peer_id, false);
this.event(_EVENTS.stopAudio);
break;
case mediaType.video:
this.setIsVideo(false);
this.event(_EVENTS.stopVideo);
break;
case mediaType.screen:
this.setIsScreen(false);
this.event(_EVENTS.stopScreen);
break;
default:
break;
}
this.sound('left');
}
async produceScreenAudio(stream) {
try {
if (this.producerLabel.has(mediaType.audioTab)) {
return console.warn('Producer already exists for this type ' + mediaType.audioTab);
}
const track = stream.getAudioTracks()[0];
const params = {
track,
appData: {
mediaType: mediaType.audio,
},
};
const producerSa = await this.producerTransport.produce(params);
console.log('PRODUCER SCREEN AUDIO', producerSa);
this.producers.set(producerSa.id, producerSa);
this.producerLabel.set(mediaType.audioTab, producerSa.id);
console.log('[produceScreenAudio] - PRODUCER LABEL', this.producerLabel);
await this.handleProducer(producerSa.id, mediaType.audio, stream);
producerSa.on('trackended', () => {
this.closeProducer(mediaType.audioTab, 'trackended');
});
producerSa.on('transportclose', () => {
this.closeProducer(mediaType.audioTab, 'transportclose');
});
producerSa.on('close', () => {
this.closeProducer(mediaType.audioTab, 'close');
});
} catch (err) {
console.error('Produce Screen Audio error:', err);
}
}
// ####################################################
// REMOVE PRODUCER VIDEO/AUDIO
// ####################################################
removeVideoProducer(video, event) {
const d = this.getId(video.id + '__video');
const vb = this.getId(video.id + '__vb');
video.srcObject.getTracks().forEach(function (track) {
track.stop();
});
video.parentNode.removeChild(video);
d.parentNode.removeChild(d);
vb.parentNode.removeChild(vb);
handleAspectRatio();
console.log(`[${event}] Video-element-count`, this.videoMediaContainer.childElementCount);
}
removeAudioProducer(audio, event) {
audio.srcObject.getTracks().forEach(function (track) {
track.stop();
});
audio.parentNode.removeChild(audio);
console.log(`[${event}] audio-element-count`, this.localAudioEl.childElementCount);
}
// ####################################################
// CONSUMER
// ####################################################
async consume(producer_id, peer_name, peer_info, type) {
try {
wbUpdate();
this.editorUpdate();
const { consumer, stream, kind } = await this.getConsumeStream(producer_id, peer_info.peer_id, type);
console.log('CONSUMER MEDIA TYPE ----> ' + type);
console.log('CONSUMER', consumer);
this.consumers.set(consumer.id, consumer);
await this.handleConsumer(consumer.id, type, stream, peer_name, peer_info);
// https://mediasoup.discourse.group/t/create-server-side-consumers-with-paused-true/244
try {
const response = await this.socket.request('resumeConsumer', { consumer_id: consumer.id, type });
console.log('Consumer resumed', response);
} catch (error) {
console.error('Error resuming consumer', error);
}
consumer.on('trackended', () => {
console.log('Consumer track end', { id: consumer.id, type });
this.removeConsumer(consumer.id, consumer.kind);
});
consumer.on('transportclose', () => {
console.log('Consumer transport close', { id: consumer.id, type });
this.removeConsumer(consumer.id, consumer.kind);
});
if (kind === 'video' && isParticipantsListOpen) {
await getRoomParticipants();
}
} catch (error) {
console.error('Error in consume', error);
popupHtmlMessage(null, image.network, 'Consume', error, 'center', false, false);
}
}
async getConsumeStream(producerId, peer_id, type) {
if (!this.device) {
throw new Error('Device not initialized');
}
// Check if consumer transport exists
if (!this.consumerTransport) {
throw new Error('Consumer transport not initialized');
}
const { rtpCapabilities } = this.device;
const data = await this.socket.request('consume', {
consumerTransportId: this.consumerTransport.id,
rtpCapabilities,
producerId,
type,
});
const { id, kind, rtpParameters } = data;
const codecOptions = {};
const streamId = peer_id + (type == mediaType.screen ? '-screen-sharing' : '-mic-webcam');
const consumer = await this.consumerTransport.consume({
id,
producerId,
kind,
rtpParameters,
codecOptions,
streamId,
});
const stream = new MediaStream();
stream.addTrack(consumer.track);
return {
consumer,
stream,
kind,
};
}
async handleConsumer(id, type, stream, peer_name, peer_info) {
let elem, vb, d, p, i, cm, au, pip, fs, ts, sf, sm, sv, gl, ban, ko, pb, pm, pv, pn, ha, mv;
let eDiv, eBtn, eVc; // expand buttons
console.log('PEER-INFO', peer_info);
const remotePeerId = peer_info.peer_id;
const remoteIsScreen = type == mediaType.screen;
const remotePeerAudio = peer_info.peer_audio;
const remotePeerAudioVolume = peer_info.peer_audio_volume;
const remotePrivacyOn = peer_info.peer_video_privacy;
const remotePeerPresenter = peer_info.peer_presenter;
switch (type) {
case mediaType.video:
case mediaType.screen:
this.removeVideoOff(remotePeerId);
d = document.createElement('div');
d.className = 'Camera';
d.id = id + '__video';
elem = document.createElement('video');
elem.setAttribute('id', id);
elem.setAttribute('volumeBar', remotePeerId + '___pVolume');
!remoteIsScreen && elem.setAttribute('name', remotePeerId);
elem.setAttribute('playsinline', true);
elem.controls = isVideoControlsOn;
elem.autoplay = true;
elem.className = '';
elem.poster = image.poster;
elem.style.objectFit = remoteIsScreen || isBroadcastingEnabled ? 'contain' : 'var(--videoObjFit)';
vb = document.createElement('div');
vb.id = id + '__vb';
vb.className = 'videoMenuBar hidden';
eDiv = document.createElement('div');
eDiv.className = 'expand-video';
eBtn = this.createButton(
remotePeerId + (type === mediaType.screen ? '_screen_' : '_video_') + '_expandBtn',
html.expand
);
eVc = document.createElement('div');
eVc.className = 'expand-video-content';
eVc.id = remotePeerId + (type === mediaType.screen ? '_screen_' : '_video_') + '_videoExpandContent';
pip = this.createButton(id + '__pictureInPicture', html.pip);
mv = this.createButton(id + '__videoMirror', html.mirror);
fs = this.createButton(id + '__fullScreen', html.fullScreen);
ts = this.createButton(id + '__snapshot', html.snapshot);
pn = this.createButton(id + '__pin', html.pin);
ha = this.createButton(id + '__hideALL', html.hideALL + ' focusMode');
sf = this.createButton(id + '___' + remotePeerId + '___sendFile', html.sendFile);
sm = this.createButton(id + '___' + remotePeerId + '___sendMsg', html.sendMsg);
sv = this.createButton(id + '___' + remotePeerId + '___sendVideo', html.sendVideo);
cm = this.createButton(id + '___' + remotePeerId + '___video', html.videoOn);
au = this.createButton(remotePeerId + '__audio', remotePeerAudio ? html.audioOn : html.audioOff);
gl = this.createButton(id + '___' + remotePeerId + '___geoLocation', html.geolocation);
ban = this.createButton(id + '___' + remotePeerId + '___ban', html.ban);
ko = this.createButton(id + '___' + remotePeerId + '___kickOut', html.kickOut);
i = document.createElement('i');
i.id = remotePeerId + '__hand';
i.className = html.userHand;
p = document.createElement('p');
p.id = remotePeerId + '__name';
p.className = html.userName;
p.innerText = (remotePeerPresenter ? '⭐️ ' : '') + peer_name;
pm = document.createElement('div');
pb = document.createElement('div');
pm.setAttribute('id', remotePeerId + '__pitchMeter');
pb.setAttribute('id', remotePeerId + '__pitchBar');
pm.className = 'speechbar';
pb.className = 'bar';
pb.style.height = '1%';
pm.appendChild(pb);
const peerNameHeader = document.createElement('div');
peerNameHeader.className = 'peer-name-header';
const peerNameContainer = document.createElement('div');
peerNameContainer.className = 'peer-name-container';
const peerNameSpan = document.createElement('span');
peerNameSpan.className = 'peer-name';
peerNameSpan.textContent = peer_name;
peerNameContainer.appendChild(peerNameSpan);
pv = document.createElement('input');
pv.id = remotePeerId + '___pVolume';
pv.type = 'range';
pv.min = 0;
pv.max = 100;
pv.value = 100;
BUTTONS.consumerVideo.audioVolumeInput && peerNameContainer.appendChild(pv);
peerNameHeader.appendChild(peerNameContainer);
vb.appendChild(peerNameHeader);
eVc.appendChild(peerNameHeader);
const buttonGroup = document.createElement('div');
buttonGroup.className = 'button-group';
BUTTONS.consumerVideo.sendMessageButton && buttonGroup.appendChild(sm);
BUTTONS.consumerVideo.sendFileButton && buttonGroup.appendChild(sf);
BUTTONS.consumerVideo.sendVideoButton && buttonGroup.appendChild(sv);
BUTTONS.consumerVideo.geolocationButton && buttonGroup.appendChild(gl);
BUTTONS.consumerVideo.banButton && buttonGroup.appendChild(ban);
BUTTONS.consumerVideo.ejectButton && buttonGroup.appendChild(ko);
eVc.appendChild(buttonGroup);
vb.appendChild(eBtn);
vb.appendChild(au);
vb.appendChild(cm);
BUTTONS.consumerVideo.snapShotButton && vb.appendChild(ts);
BUTTONS.consumerVideo.videoPictureInPicture &&
this.isVideoPictureInPictureSupported &&
vb.appendChild(pip);
BUTTONS.consumerVideo.videoMirrorButton && vb.appendChild(mv);
BUTTONS.consumerVideo.fullScreenButton && this.isVideoFullScreenSupported && vb.appendChild(fs);
BUTTONS.consumerVideo.focusVideoButton && vb.appendChild(ha);
if (!this.isMobileDevice) vb.appendChild(pn);
d.appendChild(elem);
d.appendChild(i);
d.appendChild(p);
d.appendChild(pm);
if (this.isMobileDevice) {
vb.classList.add('mobile-floating');
document.body.appendChild(eVc);
document.body.appendChild(vb);
} else {
vb.classList.remove('mobile-floating');
d.appendChild(eVc);
d.appendChild(vb);
}
vb.addEventListener('click', (e) => e.stopPropagation());
this.videoMediaContainer.appendChild(d);
await this.attachMediaStream(elem, stream, type, 'Consumer');
this.isVideoPictureInPictureSupported && this.handlePIP(elem.id, pip.id);
this.isVideoFullScreenSupported && this.handleFS(elem.id, fs.id);
this.handleVB(d.id, vb.id, eBtn.id, eVc.id);
this.handleDD(elem.id, remotePeerId);
this.handleTS(elem.id, ts.id);
this.handleMV(elem.id, mv.id);
this.handleSF(sf.id);
this.handleHA(ha.id, d.id);
this.handleSM(sm.id, peer_name);
this.handleSV(sv.id);
BUTTONS.consumerVideo.muteVideoButton && this.handleCM(cm.id);
BUTTONS.consumerVideo.muteAudioButton && this.handleAU(au.id);
this.handleCV(id + '___' + pv.id);
this.handleGL(gl.id);
this.handleBAN(ban.id);
this.handleKO(ko.id);
this.handlePN(elem.id, pn.id, d.id, remoteIsScreen, false, eVc.id);
this.handleZV(elem.id, d.id, remotePeerId);
this.popupPeerInfo(p.id, peer_info);
this.checkPeerInfoStatus(peer_info);
if (!remoteIsScreen && remotePrivacyOn) this.setVideoPrivacyStatus(remotePeerId, remotePrivacyOn);
if (remoteIsScreen && !isHideALLVideosActive) pn.click();
if (isHideALLVideosActive) {
isHideALLVideosActive = false;
const children = this.videoMediaContainer.children;
const btnsHA = document.querySelectorAll('.focusMode');
for (let child of children) {
child.style.display = 'block';
}
btnsHA.forEach((btn) => {
btn.style.color = 'white';
});
}
if (!this.isMobileDevice) {
this.setTippy(pn.id, 'Toggle Pin', 'bottom');
this.setTippy(ha.id, 'Toggle Focus mode', 'bottom');
this.setTippy(pip.id, 'Toggle picture in picture', 'bottom');
this.setTippy(mv.id, 'Toggle mirror', 'bottom');
this.setTippy(ts.id, 'Snapshot', 'bottom');
this.setTippy(sf.id, 'Send file', 'bottom');
this.setTippy(sm.id, 'Send message', 'bottom');
this.setTippy(sv.id, 'Send video', 'bottom');
this.setTippy(cm.id, 'Hide', 'bottom');
this.setTippy(au.id, 'Mute', 'bottom');
this.setTippy(pv.id, '🔊 Volume', 'bottom');
this.setTippy(gl.id, 'Geolocation', 'bottom');
this.setTippy(ban.id, 'Ban', 'bottom');
this.setTippy(ko.id, 'Eject', 'bottom');
}
// Use helper function to set audio volume
this.setAV(
this.audioConsumers.get(remotePeerId + '___pVolume'),
remotePeerId + '___pVolume',
remotePeerAudioVolume,
true
);
this.setPeerAudio(remotePeerId, remotePeerAudio);
handleAspectRatio();
console.log('[addConsumer] Video-element-count', this.videoMediaContainer.childElementCount);
this.sound('joined');
break;
case mediaType.audio:
elem = document.createElement('audio');
elem.setAttribute('id', id);
elem.setAttribute('volumeBar', remotePeerId + '___pVolume');
elem.autoplay = true;
elem.audio = 1.0;
this.remoteAudioEl.appendChild(elem);
await this.attachMediaStream(elem, stream, type, 'Consumer');
// Store audio consumer and set volume
const audioConsumerId = remotePeerId + '___pVolume';
this.audioConsumers.set(audioConsumerId, id);
// Use helper function to set audio volume
this.setAV(id, audioConsumerId, remotePeerAudioVolume, true);
this.handleCV(id + '___' + audioConsumerId);
this.setPeerAudio(remotePeerId, remotePeerAudio);
if (sinkId && speakerSelect.value) {
this.changeAudioDestination(elem);
}
//elem.addEventListener('play', () => { elem.volume = 0.1 });
console.log('[Add audioConsumers]', this.audioConsumers);
break;
default:
break;
}
return elem;
}
removeConsumer(consumer_id, consumer_kind) {
if (!this.consumers.get(consumer_id)) return;
console.log('Remove consumer', { consumer_id: consumer_id, consumer_kind: consumer_kind });
const elem = this.getId(consumer_id);
if (elem) {
elem.srcObject.getTracks().forEach(function (track) {
track.stop();
});
elem.parentNode.removeChild(elem);
}
if (consumer_kind === 'video') {
const d = this.getId(consumer_id + '__video');
const vb = this.getId(consumer_id + '__vb');
if (d) {
// Check if video is in focus-mode...
if (d.hasAttribute('focus-mode')) {
const dhaBtn = this.getId(consumer_id + '__hideALL');
if (dhaBtn) {
dhaBtn.click();
}
}
d.parentNode.removeChild(d);
vb.parentNode.removeChild(vb);
//alert(this.pinnedVideoPlayerId + '==' + consumer_id);
if (this.isVideoPinned && this.pinnedVideoPlayerId == consumer_id) {
this.removeVideoPinMediaContainer();
console.log('Remove pin container due the Consumer close', {
consumer_id: consumer_id,
consumer_kind: consumer_kind,
});
}
}
handleAspectRatio();
console.log(
'[removeConsumer - ' + consumer_kind + '] Video-element-count',
this.videoMediaContainer.childElementCount
);
}
if (consumer_kind === 'audio') {
const audioConsumerPlayerId = this.getMapKeyByValue(this.audioConsumers, consumer_id);
if (audioConsumerPlayerId) {
const inputPv = this.getId(audioConsumerPlayerId);
if (inputPv) inputPv.style.display = 'none';
this.audioConsumers.delete(audioConsumerPlayerId);
console.log('Remove audio Consumer', {
consumer_id: consumer_id,
audioConsumerPlayerId: audioConsumerPlayerId,
audioConsumers: this.audioConsumers,
});
}
}
this.consumers.get(consumer_id).close();
this.consumers.delete(consumer_id);
this.sound('left');
}
// ####################################################
// HANDLE VIDEO OFF
// ####################################################
setVideoOff(peer_info, remotePeer = false) {
//console.log('setVideoOff', peer_info);
let d, vb, i, h, au, sf, sm, sv, gl, ban, ko, p, pm, pb, pv;
const { peer_id, peer_name, peer_avatar, peer_audio, peer_presenter } = peer_info;
this.removeVideoOff(peer_id);
d = document.createElement('div');
d.className = 'Camera';
d.id = peer_id + '__videoOff';
vb = document.createElement('div');
vb.id = peer_id + '__vb';
vb.className = 'videoMenuBar hidden';
au = this.createButton(peer_id + '__audio', peer_audio ? html.audioOn : html.audioOff);
pv = document.createElement('input');
pv.id = peer_id + '___pVolume';
pv.type = 'range';
pv.min = 0;
pv.max = 100;
pv.value = 100;
if (remotePeer) {
sf = this.createButton('remotePeer___' + peer_id + '___sendFile', html.sendFile);
sm = this.createButton('remotePeer___' + peer_id + '___sendMsg', html.sendMsg);
sv = this.createButton('remotePeer___' + peer_id + '___sendVideo', html.sendVideo);
gl = this.createButton('remotePeer___' + peer_id + '___geoLocation', html.geolocation);
ban = this.createButton('remotePeer___' + peer_id + '___ban', html.ban);
ko = this.createButton('remotePeer___' + peer_id + '___kickOut', html.kickOut);
}
i = document.createElement('img');
i.className = 'videoAvatarImage center'; // pulsate
i.id = peer_id + '__img';
p = document.createElement('p');
p.id = peer_id + '__name';
p.className = html.userName;
p.innerText = (peer_presenter ? '⭐️ ' : '') + peer_name + (remotePeer ? '' : ' (me) ');
h = document.createElement('i');
h.id = peer_id + '__hand';
h.className = html.userHand;
pm = document.createElement('div');
pb = document.createElement('div');
pm.setAttribute('id', peer_id + '__pitchMeter');
pb.setAttribute('id', peer_id + '__pitchBar');
pm.className = 'speechbar';
pb.className = 'bar';
pb.style.height = '1%';
pm.appendChild(pb);
if (remotePeer) {
BUTTONS.videoOff.ejectButton && vb.appendChild(ko);
BUTTONS.videoOff.banButton && vb.appendChild(ban);
BUTTONS.videoOff.geolocationButton && vb.appendChild(gl);
BUTTONS.videoOff.sendVideoButton && vb.appendChild(sv);
BUTTONS.videoOff.sendFileButton && vb.appendChild(sf);
BUTTONS.videoOff.sendMessageButton && vb.appendChild(sm);
}
BUTTONS.videoOff.audioVolumeInput && vb.appendChild(pv);
vb.appendChild(au);
d.appendChild(i);
d.appendChild(p);
d.appendChild(h);
d.appendChild(pm);
const hideVideoMenu = () => {
if (vb && !vb.classList.contains('hidden')) {
hide(vb);
setCamerasBorderNone();
}
};
if (this.isMobileDevice) {
vb.classList.add('mobile-floating');
document.body.appendChild(vb);
} else {
vb.classList.remove('mobile-floating');
d.appendChild(vb);
d.addEventListener('mouseleave', hideVideoMenu);
}
vb.addEventListener('click', (e) => e.stopPropagation());
this.videoMediaContainer.appendChild(d);
BUTTONS.videoOff.muteAudioButton && this.handleAU(au.id);
if (remotePeer) {
this.handleCV('remotePeer___' + pv.id);
this.handleSM(sm.id);
this.handleSF(sf.id);
this.handleSV(sv.id);
this.handleGL(gl.id);
this.handleBAN(ban.id);
this.handleKO(ko.id);
} else {
this.handlePV(this.audioConsumers.get(pv.id) + '___' + pv.id);
}
this.handleVB(d.id, vb.id);
this.handleDD(d.id, peer_id, !remotePeer);
this.popupPeerInfo(p.id, peer_info);
this.checkPeerInfoStatus(peer_info);
this.setVideoAvatarImgName(i.id, peer_name, peer_avatar);
this.getId(i.id).style.display = 'block';
if (isParticipantsListOpen) getRoomParticipants();
if (!this.isMobileDevice && remotePeer) {
this.setTippy(sm.id, 'Send message', 'bottom');
this.setTippy(sf.id, 'Send file', 'bottom');
this.setTippy(sv.id, 'Send video', 'bottom');
this.setTippy(au.id, 'Mute', 'bottom');
this.setTippy(pv.id, '🔊 Volume', 'bottom');
this.setTippy(gl.id, 'Geolocation', 'bottom');
this.setTippy(ban.id, 'Ban', 'bottom');
this.setTippy(ko.id, 'Eject', 'bottom');
}
remotePeer ? this.setPeerAudio(peer_id, peer_audio) : this.setIsAudio(peer_id, peer_audio);
handleAspectRatio();
console.log('[setVideoOff] Video-element-count', this.videoMediaContainer.childElementCount);
wbUpdate();
this.editorUpdate();
this.handleHideMe();
}
removeVideoOff(peer_id) {
const pvOff = this.getId(peer_id + '__videoOff');
const vb = this.getId(peer_id + '__vb');
if (vb) vb.parentNode.removeChild(vb);
if (pvOff) {
pvOff.parentNode.removeChild(pvOff);
handleAspectRatio();
console.log('[removeVideoOff] Video-element-count', this.videoMediaContainer.childElementCount);
if (peer_id != this.peer_id) this.sound('left');
}
}
// ####################################################
// SHARE SCREEN ON JOIN
// ####################################################
shareScreen() {
if (!this.isMobileDevice && (navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia)) {
this.sound('open');
// startScreenButton.click(); // Chrome - Opera - Edge - Brave
// handle error: getDisplayMedia requires transient activation from a user gesture on Safari - FireFox
Swal.fire({
background: swalBackground,
position: 'center',
icon: 'question',
text: 'Do you want to share your screen?',
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
startScreenButton.click();
console.log('11 ----> Screen is on');
} else {
console.log('11 ----> Screen is on');
}
});
} else {
console.log('11 ----> Screen is off');
}
}
// ####################################################
// EXIT ROOM
// ####################################################
exit(offline = false) {
if (VideoAI.active) this.stopSession();
if (this.rtmpFilestreamer) this.stopRTMP();
if (this.rtmpUrlstreamer) this.stopRTMPfromURL();
if (this.RNNoiseProcessor) this.disableRNNoiseSuppression();
const clean = () => {
this._isConnected = false;
if (this.consumerTransport) this.consumerTransport.close();
if (this.producerTransport) this.producerTransport.close();
this.socket.off('disconnect');
this.socket.off('newProducers');
this.socket.off('consumerClosed');
};
if (!offline) {
this.socket
.request('exitRoom')
.then((e) => console.log('Exit Room', e))
.catch((e) => console.warn('Exit Room ', e))
.finally(() => {
clean();
this.event(_EVENTS.exitRoom);
});
} else {
clean();
}
}
exitRoom(disconnectAll = false) {
if (isExiting) return;
isExiting = true;
const switchDisconnectAllOnLeave = getId('switchDisconnectAllOnLeave');
if (isPresenter && (disconnectAll || (switchDisconnectAllOnLeave && switchDisconnectAllOnLeave.checked))) {
this.ejectAllOnLeave();
}
this.exit();
setTimeout(() => {
isExiting = false;
}, 2000);
}
// ####################################################
// EJECT ALL ON LEAVE ROOM
// ####################################################
ejectAllOnLeave() {
const cmd = {
type: 'ejectAll',
peer_name: this.peer_name,
peer_uuid: this.peer_uuid,
broadcast: true,
};
this.emitCmd(cmd);
}
// ####################################################
// HELPERS
// ####################################################
async attachMediaStream(elem, stream, type, who) {
let track;
switch (type) {
case mediaType.audio:
track = stream.getAudioTracks()[0];
break;
case mediaType.video:
case mediaType.screen:
track = stream.getVideoTracks()[0];
break;
default:
break;
}
const consumerStream = new MediaStream();
consumerStream.addTrack(track);
elem.srcObject = consumerStream;
console.log(who + ' Success attached media ' + type);
}
hasUserActivation() {
if (navigator.userActivation) return !!navigator.userActivation.isActive;
if ('hasTransientUserActivation' in document) return !!document.hasTransientUserActivation;
return false;
}
runOnNextUserActivation(callback) {
let fired = false;
const fire = (e) => {
if (fired) return; // Prevent duplicate calls
fired = true;
try {
// Call synchronously to keep the user-activation
callback(e);
} catch (err) {
console.error('runOnNextUserActivation callback error:', err);
}
};
const cleanup = () => {
window.removeEventListener('pointerdown', fire, true);
window.removeEventListener('click', fire, true);
window.removeEventListener('mousedown', fire, true);
window.removeEventListener('touchstart', fire, true);
window.removeEventListener('keydown', fire, true);
};
// Note: 'once: true' auto-removes listeners, but we return cleanup for manual removal if needed
const opts = { capture: true, once: true, passive: true };
window.addEventListener('pointerdown', fire, opts);
window.addEventListener('click', fire, opts);
window.addEventListener('mousedown', fire, opts);
window.addEventListener('touchstart', fire, opts);
window.addEventListener('keydown', fire, opts);
// Return cleanup function for manual removal if needed (e.g., component unmount)
return cleanup;
}
async changeAudioDestination(audioElement = false) {
const sinkId = speakerSelect?.value;
if (!sinkId) return;
// Defer until a user gesture if needed
if (!this.hasUserActivation()) {
this.pendingSinkId = sinkId;
console.warn('Click once to apply the selected speaker');
this.runOnNextUserActivation(async () => {
const els = audioElement ? [audioElement] : this.remoteAudioEl.querySelectorAll('audio');
for (const el of els) {
await this.attachSinkId(el, this.pendingSinkId);
}
// Clear only if all succeeded or if pendingSinkId wasn't changed
if (this.pendingSinkId === sinkId) {
this.pendingSinkId = null;
}
});
return;
}
const els = audioElement ? [audioElement] : this.remoteAudioEl.querySelectorAll('audio');
for (const el of els) {
await this.attachSinkId(el, sinkId);
}
}
async attachSinkId(elem, sinkId) {
if (typeof elem.setSinkId !== 'function') {
const error = `Browser doesn't support output device selection.`;
console.warn(error);
this.userLog('error', error, 'top-end', 6000);
return;
}
return elem
.setSinkId(sinkId)
.then(() => {
console.log(`Success, audio output device attached: ${sinkId}`);
// Clear pending sink id after successful attachment
if (this.pendingSinkId === sinkId) {
this.pendingSinkId = null;
}
})
.catch((err) => {
console.error('Attach SinkId error: ', err);
const speakerSel = this.getId('speakerSelect');
if (err?.name === 'SecurityError') {
const msg = `Use HTTPS to select audio output device: ${err.message || err}`;
console.error('Attach SinkId error: ', msg);
this.userLog('error', msg, 'top-end', 6000);
} else if (err?.name === 'NotAllowedError' || /user gesture/i.test(err?.message || '')) {
// Retry on next user gesture
this.userLog('info', 'Click once to allow changing the speaker', 'top-end', 4000);
this.pendingSinkId = sinkId;
this.runOnNextUserActivation(() => {
// Check if pendingSinkId is still set before retrying
if (this.pendingSinkId === sinkId) {
this.attachSinkId(elem, this.pendingSinkId);
}
});
} else {
this.userLog('warning', 'Attach SinkId error', err, 'top-end', 6000);
}
if (speakerSel) speakerSel.selectedIndex = 0;
refreshLsDevices();
});
}
event(evt) {
if (this.eventListeners.has(evt)) {
this.eventListeners.get(evt).forEach((callback) => callback());
}
}
on(evt, callback) {
this.eventListeners.get(evt).push(callback);
}
// ####################################################
// SET
// ####################################################
setTippy(elem, content, placement, allowHTML = false) {
if (this.isMobileDevice) return;
const element = this.getId(elem);
if (element) {
if (element._tippy) {
element._tippy.destroy();
}
try {
tippy(element, {
content: content,
placement: placement,
allowHTML: allowHTML,
});
} catch (err) {
console.error('setTippy error', err.message);
}
} else {
console.warn('setTippy element not found with content', content);
}
}
setVideoAvatarImgName(elemId, peer_name, peer_avatar = false) {
let elem = this.getId(elemId);
if (peer_avatar && rc.isImageURL(peer_avatar)) {
elem.setAttribute('src', peer_avatar);
} else if (cfg.useAvatarSvg) {
rc.isValidEmail(peer_name)
? elem.setAttribute('src', this.genGravatar(peer_name))
: elem.setAttribute('src', this.genAvatarSvg(peer_name, 250));
} else {
elem.setAttribute('src', image.avatar);
}
}
genGravatar(email, size = false) {
const hash = md5(email.toLowerCase().trim());
const gravatarURL = `https://www.gravatar.com/avatar/${hash}` + (size ? `?s=${size}` : '?s=250') + '?d=404';
return gravatarURL;
function md5(input) {
return CryptoJS.MD5(input).toString();
}
}
isValidEmail(email) {
const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
return emailRegex.test(email);
}
genAvatarSvg(peerName, avatarImgSize) {
const charCodeRed = peerName.charCodeAt(0);
const charCodeGreen = peerName.charCodeAt(1) || charCodeRed;
const red = Math.pow(charCodeRed, 7) % 200;
const green = Math.pow(charCodeGreen, 7) % 200;
const blue = (red + green) % 200;
const bgColor = `rgb(${red}, ${green}, ${blue})`;
const textColor = '#ffffff';
const svg = `
`;
return 'data:image/svg+xml,' + svg.replace(/#/g, '%23').replace(/"/g, "'").replace(/&/g, '&');
}
setPeerAudio(peer_id, status) {
console.log('Set peer audio enabled: ' + status);
const audioStatus = this.getPeerAudioBtn(peer_id); // producer, consumers
const audioVolume = this.getPeerAudioVolumeBar(peer_id); // consumers
if (audioStatus) audioStatus.className = status ? html.audioOn : html.audioOff;
if (audioVolume) status ? show(audioVolume) : hide(audioVolume);
}
setIsAudio(peer_id, status) {
if (!isBroadcastingEnabled || (isBroadcastingEnabled && isPresenter)) {
console.log('Set local audio enabled: ' + status);
this.peer_info.peer_audio = status;
const audioStatus = this.getPeerAudioBtn(peer_id); // producer, consumers
const audioVolume = this.getPeerAudioVolumeBar(peer_id); // consumers
if (audioStatus) audioStatus.className = status ? html.audioOn : html.audioOff;
if (audioVolume) status ? show(audioVolume) : hide(audioVolume);
}
}
setIsVideo(status) {
if (!isBroadcastingEnabled || (isBroadcastingEnabled && isPresenter)) {
this.peer_info.peer_video = status;
if (!this.peer_info.peer_video) {
console.log('Set local video enabled: ' + status);
this.setVideoOff(this.peer_info, false);
this.sendVideoOff();
}
}
}
setIsScreen(status) {
if (!isBroadcastingEnabled || (isBroadcastingEnabled && isPresenter)) {
this.peer_info.peer_screen = status;
if (!this.peer_info.peer_screen && !this.peer_info.peer_video) {
console.log('Set local screen enabled: ' + status);
this.setVideoOff(this.peer_info, false);
this.sendVideoOff();
}
}
}
sendVideoOff() {
this.socket.emit('setVideoOff', this.peer_info);
}
// ####################################################
// GET
// ####################################################
isConnected() {
return this._isConnected;
}
isRecording() {
return this._isRecording;
}
static get mediaType() {
return mediaType;
}
static get EVENTS() {
return _EVENTS;
}
getTimeNow() {
return new Date().toTimeString().split(' ')[0];
}
getId(id) {
return document.getElementById(id);
}
getName(name) {
return document.getElementsByName(name)[0];
}
getEcN(cn) {
return document.getElementsByClassName(cn);
}
async getRoomInfo() {
let room_info = await this.socket.request('getRoomInfo');
return room_info;
}
refreshParticipantsCount() {
this.socket.emit('refreshParticipantsCount');
}
getPeerAudioBtn(peer_id) {
return this.getId(peer_id + '__audio');
}
getPeerAudioVolumeBar(peer_id) {
return this.getId(peer_id + '___pVolume');
}
getPeerHandBtn(peer_id) {
return this.getId(peer_id + '__hand');
}
getMapKeyByValue(map, searchValue) {
for (let [key, value] of map.entries()) {
if (value === searchValue) return key;
}
}
getSelectedIndexValue(elem) {
return elem.options[elem.selectedIndex].value;
}
// ####################################################
// UTILITY
// ####################################################
async sound(name, force = false, path = '../sounds/', ext = '.wav') {
if (!isSoundEnabled && !force) return;
let sound = path + name + ext;
let audio = new Audio(sound);
try {
audio.volume = 0.5;
await audio.play();
} catch (err) {
return false;
}
}
userLog(icon, message, position, timer = 5000) {
const Toast = Swal.mixin({
background: swalBackground,
toast: true,
position: position,
showConfirmButton: false,
timer: timer,
timerProgressBar: true,
});
switch (icon) {
case 'html':
Toast.fire({
icon: icon,
html: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
break;
default:
Toast.fire({
icon: icon,
title: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
}
toast(icon, title, text, position = 'top-end', timer = 5000, sound = false) {
if (sound) this.sound('alert');
const Toast = Swal.mixin({
toast: true,
position: position,
showConfirmButton: false,
timer: timer,
timerProgressBar: true,
background: swalBackground,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
Toast.fire({
icon: icon,
title: title,
text: text,
});
}
msgPopup(type, message, timer = 3000, position = 'center') {
switch (type) {
case 'warning':
case 'error':
Swal.fire({
background: swalBackground,
position: position,
icon: type,
title: type,
text: message,
showClass: { popup: 'animate__animated animate__rubberBand' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
this.sound('alert');
break;
case 'info':
case 'success':
Swal.fire({
background: swalBackground,
position: position,
icon: type,
title: type,
text: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
break;
case 'html':
Swal.fire({
background: swalBackground,
position: position,
icon: type,
html: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
break;
case 'toast':
const Toast = Swal.mixin({
background: swalBackground,
position: 'top-end',
icon: 'info',
showConfirmButton: false,
timerProgressBar: true,
toast: true,
timer: timer,
});
Toast.fire({
icon: 'info',
title: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
break;
// ......
default:
alert(message);
}
}
msgHTML(data, icon, imageUrl, title, html, position = 'center') {
switch (data.type) {
case 'recording':
switch (data.action) {
case enums.recording.started:
case enums.recording.start:
html = html + '
Your presence implies you agree to being recorded';
toastMessage(6000);
break;
case enums.recording.stop:
toastMessage(3000);
break;
//...
default:
break;
}
if (!this.speechInMessages) this.speechText(`${data.peer_name} ${data.action}`);
break;
//...
default:
defaultMessage();
break;
}
// TOAST less invasive
function toastMessage(duration = 3000) {
const Toast = Swal.mixin({
background: swalBackground,
position: 'top-end',
icon: icon,
showConfirmButton: false,
timerProgressBar: true,
toast: true,
timer: duration,
});
Toast.fire({
title: title,
html: html,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
// DEFAULT
function defaultMessage() {
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
position: position,
icon: icon,
imageUrl: imageUrl,
title: title,
html: html,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
//...
}
thereAreParticipants() {
// console.log('participantsCount ---->', participantsCount);
return this.consumers.size > 0 || participantsCount > 1;
}
// ####################################################
// MY SETTINGS
// ####################################################
toggleMySettings() {
let mySettings = this.getId('mySettings');
mySettings.style.top = '50%';
mySettings.style.left = '50%';
if (this.isMobileDevice) {
mySettings.style.width = '100%';
mySettings.style.height = '100%';
}
mySettings.classList.toggle('show');
this.isMySettingsOpen = !this.isMySettingsOpen;
this.videoMediaContainer.style.opacity = this.isMySettingsOpen ? 0.3 : 1;
}
openTab(evt, tabName) {
let i, tabcontent, tablinks;
tabcontent = this.getEcN('tabcontent');
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = 'none';
}
tablinks = this.getEcN('tablinks');
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(' active', '');
}
this.getId(tabName).style.display = 'block';
evt.currentTarget.className += ' active';
}
changeBtnsBarPosition(position) {
switch (position) {
case 'vertical':
document.documentElement.style.setProperty('--btns-top', '50%');
document.documentElement.style.setProperty('--btns-right', '0%');
document.documentElement.style.setProperty('--btns-left', '10px');
document.documentElement.style.setProperty('--btns-margin-left', '0px');
document.documentElement.style.setProperty('--btns-width', '60px');
document.documentElement.style.setProperty('--btns-flex-direction', 'column');
// bottomButtons horizontally
document.documentElement.style.setProperty('--bottom-btns-top', 'auto');
document.documentElement.style.setProperty('--bottom-btns-left', '50%');
document.documentElement.style.setProperty('--bottom-btns-bottom', '0');
document.documentElement.style.setProperty('--bottom-btns-translate-X', '-50%');
document.documentElement.style.setProperty('--bottom-btns-translate-Y', '0%');
document.documentElement.style.setProperty('--bottom-btns-margin-bottom', '16px');
document.documentElement.style.setProperty('--bottom-btns-flex-direction', 'row');
break;
case 'horizontal':
document.documentElement.style.setProperty('--btns-top', '95%');
document.documentElement.style.setProperty('--btns-right', '25%');
document.documentElement.style.setProperty('--btns-left', '50%');
document.documentElement.style.setProperty('--btns-margin-left', '-240px');
document.documentElement.style.setProperty('--btns-width', '480px');
document.documentElement.style.setProperty('--btns-flex-direction', 'row');
// bottomButtons vertically
document.documentElement.style.setProperty('--bottom-btns-top', '50%');
document.documentElement.style.setProperty('--bottom-btns-left', '15px');
document.documentElement.style.setProperty('--bottom-btns-bottom', 'auto');
document.documentElement.style.setProperty('--bottom-btns-translate-X', '0%');
document.documentElement.style.setProperty('--bottom-btns-translate-Y', '-50%');
document.documentElement.style.setProperty('--bottom-btns-margin-bottom', '0');
document.documentElement.style.setProperty('--bottom-btns-flex-direction', 'column');
break;
default:
break;
}
}
// ####################################################
// PICTURE IN PICTURE
// ####################################################
handlePIP(elemId, pipId) {
let videoPlayer = this.getId(elemId);
let btnPIP = this.getId(pipId);
if (btnPIP) {
btnPIP.addEventListener('click', () => {
if (videoPlayer.pictureInPictureElement) {
videoPlayer.exitPictureInPicture();
} else if (document.pictureInPictureEnabled) {
videoPlayer.requestPictureInPicture().catch((error) => {
console.error('Failed to enter Picture-in-Picture mode:', error);
this.userLog('warning', error.message, 'top-end', 6000);
elemDisplay(btnPIP.id, false);
});
}
});
}
if (videoPlayer) {
videoPlayer.addEventListener('leavepictureinpicture', (event) => {
console.log('Exited PiP mode');
if (videoPlayer.paused) {
videoPlayer.play().catch((error) => {
console.error('Error playing video after exit PIP mode:', error);
});
}
});
}
}
// ####################################################
// HANDLE DOCUMENT PIP
// ####################################################
async toggleDocumentPIP() {
if (documentPictureInPicture.window) {
documentPictureInPicture.window.close();
console.log('DOCUMENT PIP close');
return;
}
await this.documentPictureInPictureOpen();
}
documentPictureInPictureClose() {
if (!showDocumentPipBtn) return;
if (documentPictureInPicture.window) {
documentPictureInPicture.window.close();
console.log('DOCUMENT PIP close');
}
}
async documentPictureInPictureOpen() {
if (!showDocumentPipBtn) return;
try {
const pipWindow = await documentPictureInPicture.requestWindow({
width: 300,
height: 720,
});
function updateCustomProperties() {
const documentStyle = getComputedStyle(document.documentElement);
pipWindow.document.documentElement.style = `
--body-bg: ${documentStyle.getPropertyValue('--body-bg')};
`;
}
updateCustomProperties();
const pipStylesheet = document.createElement('link');
const pipVideoContainer = document.createElement('div');
pipStylesheet.type = 'text/css';
pipStylesheet.rel = 'stylesheet';
pipStylesheet.href = '../css/DocumentPiP.css';
pipVideoContainer.className = 'pipVideoContainer';
pipWindow.document.head.append(pipStylesheet);
pipWindow.document.body.append(pipVideoContainer);
function cloneVideoElements() {
let foundVideo = false;
pipVideoContainer.innerHTML = '';
[...document.querySelectorAll('video')].forEach((video) => {
console.log('DOCUMENT PIP found video id -----> ' + video.id);
// No video stream detected or is video share from URL...
if (!video.srcObject || video.id === '__videoShare') return;
const videoElement = rc.getId(video.id);
const isPIPAllowed = !videoElement.classList.contains('videoCircle'); // Check if not in privacy mode
const logMessage = [rc.videoProducerId, rc.screenProducerId].includes(video.id)
? `DOCUMENT PIP PRODUCER: PiP allowed? -----> ${isPIPAllowed}`
: `DOCUMENT PIP CONSUMER: PiP allowed? -----> ${isPIPAllowed}`;
console.log(logMessage);
if (!isPIPAllowed) return;
// Video is ON and not in privacy mode continue....
foundVideo = true;
const pipVideo = document.createElement('video');
pipVideo.classList.add('pipVideo');
pipVideo.classList.toggle('mirror', video.classList.contains('mirror'));
pipVideo.srcObject = video.srcObject;
pipVideo.autoplay = true;
pipVideo.muted = true;
pipVideoContainer.append(pipVideo);
const videoElementObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
// Handle class changes in video elements
console.log(`Video ${mutation.target.id} class changed:`, mutation.target.className);
cloneVideoElements();
}
});
});
// Start observing for new videos and class changes
videoElementObserver.observe(video, { attributes: true, attributeFilter: ['class'] });
});
return foundVideo;
}
if (!cloneVideoElements()) {
rc.documentPictureInPictureClose();
return userLog('warning', 'No video allowed for Document PIP', 'top-end', 6000);
}
const videoObserver = new MutationObserver(() => {
cloneVideoElements();
});
videoObserver.observe(rc.videoMediaContainer, {
childList: true,
});
const documentObserver = new MutationObserver(() => {
updateCustomProperties();
});
documentObserver.observe(document.documentElement, {
attributeFilter: ['style'],
});
pipWindow.addEventListener('unload', () => {
videoObserver.disconnect();
documentObserver.disconnect();
});
} catch (err) {
userLog('warning', err.message, 'top-end', 6000);
}
}
// ####################################################
// FULL SCREEN
// ####################################################
isFullScreenSupported() {
const fsSupported =
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled;
fsSupported ? this.handleFullScreenEvents() : (this.getId('fullScreenButton').style.display = 'none');
return fsSupported;
}
handleFullScreenEvents() {
document.addEventListener('fullscreenchange', (e) => {
const fullscreenElement = document.fullscreenElement;
if (!fullscreenElement) {
const fullScreenIcon = this.getId('fullScreenIcon');
fullScreenIcon.className = html.fullScreenOff;
this.isDocumentOnFullScreen = false;
}
});
}
toggleRoomFullScreen() {
const fullScreenIcon = this.getId('fullScreenIcon');
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
fullScreenIcon.className = html.fullScreenOn;
this.isDocumentOnFullScreen = true;
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
fullScreenIcon.className = html.fullScreenOff;
this.isDocumentOnFullScreen = false;
}
}
}
toggleFullScreen(elem = null) {
if (this.isDocumentOnFullScreen) return;
const element = elem ? elem : document.documentElement;
const fullScreen = this.isFullScreen();
fullScreen ? this.goOutFullscreen(element) : this.goInFullscreen(element);
if (elem === null) this.isVideoOnFullScreen = fullScreen;
}
isFullScreen() {
const elementFullScreen =
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement ||
null;
if (elementFullScreen === null) return false;
return true;
}
goInFullscreen(element) {
if (element.requestFullscreen) element.requestFullscreen();
else if (element.mozRequestFullScreen) element.mozRequestFullScreen();
else if (element.webkitRequestFullscreen) element.webkitRequestFullscreen();
else if (element.msRequestFullscreen) element.msRequestFullscreen();
else this.userLog('warning', 'Full screen mode not supported by this browser on this device', 'top-end');
}
goOutFullscreen(element) {
if (element.exitFullscreen) element.exitFullscreen();
else if (element.mozCancelFullScreen) element.mozCancelFullScreen();
else if (element.webkitExitFullscreen) element.webkitExitFullscreen();
else if (element.msExitFullscreen) element.msExitFullscreen();
}
handleFS(elemId, fsId) {
let videoPlayer = this.getId(elemId);
let btnFs = this.getId(fsId);
if (videoPlayer && btnFs) {
this.setTippy(fsId, 'Full screen', 'bottom');
btnFs.addEventListener('click', () => {
if (videoPlayer.classList.contains('videoCircle')) {
return this.userLog('info', 'Full Screen not allowed if video on privacy mode', 'top-end');
}
videoPlayer.style.pointerEvents = this.isVideoOnFullScreen ? 'auto' : 'none';
this.toggleFullScreen(videoPlayer);
});
videoPlayer.addEventListener('fullscreenchange', (e) => {
if (!document.fullscreenElement) {
videoPlayer.style.pointerEvents = 'auto';
this.isVideoOnFullScreen = false;
}
});
videoPlayer.addEventListener('webkitfullscreenchange', (e) => {
if (!document.webkitIsFullScreen) {
videoPlayer.style.pointerEvents = 'auto';
this.isVideoOnFullScreen = false;
}
});
}
}
// ####################################################
// HANDLE VIDEO | OBJ FIT | CONTROLS | PIN-UNPIN
// ####################################################
handleVideoObjectFit(value) {
document.documentElement.style.setProperty('--videoObjFit', value);
}
handleVideoControls(value) {
isVideoControlsOn = value == 'on' ? true : false;
let cameras = this.getEcN('Camera');
for (let i = 0; i < cameras.length; i++) {
let cameraId = cameras[i].id.replace('__video', '');
let videoPlayer = this.getId(cameraId);
videoPlayer.hasAttribute('controls')
? videoPlayer.removeAttribute('controls')
: videoPlayer.setAttribute('controls', isVideoControlsOn);
}
}
handlePN(elemId, pnId, camId, isScreen = false, isAvatar = false, eVcId = null) {
let videoPlayer = this.getId(elemId);
let btnPn = this.getId(pnId);
let cam = this.getId(camId);
let eVc = this.getId(eVcId);
if (btnPn && videoPlayer && cam) {
btnPn.addEventListener('click', () => {
if (this.isMobileDevice) return;
this.sound('click');
this.isVideoPinned = !this.isVideoPinned;
if (this.isVideoPinned) {
if (!videoPlayer.classList.contains('videoCircle')) {
videoPlayer.style.objectFit = 'contain';
}
cam.className = '';
cam.style.width = '100%';
cam.style.height = '100%';
this.toggleVideoPin(pinVideoPosition.value);
if (eVc) this.videoPinMediaContainer.appendChild(eVc);
this.videoPinMediaContainer.appendChild(cam);
this.videoPinMediaContainer.style.display = 'block';
this.pinnedVideoPlayerId = elemId;
setColor(btnPn, 'lime');
} else {
if (this.pinnedVideoPlayerId != videoPlayer.id) {
this.isVideoPinned = true;
if (this.isScreenAllowed) return;
return this.msgPopup('toast', 'Another video seems pinned, unpin it before to pin this one');
}
if (!isScreen && !isBroadcastingEnabled) videoPlayer.style.objectFit = 'var(--videoObjFit)';
this.videoPinMediaContainer.removeChild(cam);
if (eVc) {
this.videoPinMediaContainer.removeChild(eVc);
cam.appendChild(eVc);
}
cam.className = 'Camera';
this.videoMediaContainer.appendChild(cam);
this.removeVideoPinMediaContainer();
setColor(btnPn, 'white');
}
this.resizeVideoMenuBar();
handleAspectRatio();
});
if (isAvatar && !this.isMobileDevice && this.videoMediaContainer.childElementCount > 1) btnPn.click();
}
}
toggleVideoPin(position) {
if (!this.isVideoPinned) return;
switch (position) {
case 'top':
this.videoPinMediaContainer.style.top = '25%';
this.videoPinMediaContainer.style.width = '100%';
this.videoPinMediaContainer.style.height = '75%';
this.videoMediaContainer.style.top = '0%';
this.videoMediaContainer.style.right = null;
this.videoMediaContainer.style.width = null;
this.videoMediaContainer.style.width = '100% !important';
this.videoMediaContainer.style.height = '25%';
break;
case 'vertical':
this.videoPinMediaContainer.style.top = 0;
this.videoPinMediaContainer.style.width = '75%';
this.videoPinMediaContainer.style.height = '100%';
this.videoMediaContainer.style.top = 0;
this.videoMediaContainer.style.width = '25%';
this.videoMediaContainer.style.height = '100%';
this.videoMediaContainer.style.right = 0;
break;
case 'horizontal':
this.videoPinMediaContainer.style.top = 0;
this.videoPinMediaContainer.style.width = '100%';
this.videoPinMediaContainer.style.height = '75%';
this.videoMediaContainer.style.top = '75%';
this.videoMediaContainer.style.right = null;
this.videoMediaContainer.style.width = null;
this.videoMediaContainer.style.width = '100% !important';
this.videoMediaContainer.style.height = '25%';
break;
default:
break;
}
resizeVideoMedia();
}
// ####################################################
// HANDLE VIDEO ZOOM-IN/OUT
// ####################################################
handleZV(elemId, divId, peerId) {
let videoPlayer = this.getId(elemId);
let videoWrap = this.getId(divId);
let videoPeerId = peerId;
let zoom = 1;
const ZOOM_IN_FACTOR = 1.1;
const ZOOM_OUT_FACTOR = 0.9;
const MAX_ZOOM = 15;
const MIN_ZOOM = 1;
if (this.isZoomCenterMode) {
if (videoPlayer) {
videoPlayer.addEventListener('wheel', (e) => {
e.preventDefault();
let delta = e.wheelDelta ? e.wheelDelta : -e.deltaY;
delta > 0 ? (zoom *= 1.2) : (zoom /= 1.2);
if (zoom < 1) zoom = 1;
videoPlayer.style.scale = zoom;
});
}
} else {
if (videoPlayer && videoWrap) {
videoPlayer.addEventListener('wheel', (e) => {
e.preventDefault();
if (isVideoPrivacyActive) return;
const rect = videoWrap.getBoundingClientRect();
const cursorX = e.clientX - rect.left;
const cursorY = e.clientY - rect.top;
const zoomDirection = e.deltaY > 0 ? 'zoom-out' : 'zoom-in';
const scaleFactor = zoomDirection === 'zoom-out' ? ZOOM_OUT_FACTOR : ZOOM_IN_FACTOR;
zoom *= scaleFactor;
zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
videoPlayer.style.transformOrigin = `${cursorX}px ${cursorY}px`;
videoPlayer.style.transform = `scale(${zoom})`;
videoPlayer.style.cursor = zoom === 1 ? 'pointer' : zoomDirection;
});
videoWrap.addEventListener('mouseleave', () => {
videoPlayer.style.cursor = 'pointer';
if (videoPeerId === this.peer_id) {
zoom = 1;
videoPlayer.style.transform = '';
videoPlayer.style.transformOrigin = 'center';
}
});
videoPlayer.addEventListener('mouseleave', () => {
videoPlayer.style.cursor = 'pointer';
});
}
}
}
// ####################################################
// HANDLE VIDEO AND MENU BAR
// ####################################################
handleVB(videoId, videoBarId, eBtnId = null, eVcId = null) {
const videoPlayer = this.getId(videoId);
const videoBar = this.getId(videoBarId);
const eBtn = this.getId(eBtnId);
const eVc = this.getId(eVcId);
if (eBtn && eVc) {
const showDropdown = () => {
eVc.classList.add('show');
rc.isVideoBarDropDownOpen = true;
};
const hideDropdown = () => {
eVc.classList.remove('show');
rc.isVideoBarDropDownOpen = false;
};
const handleDocumentClick = (e) => {
if (!eBtn.contains(e.target) && !eVc.contains(e.target)) {
hideDropdown();
}
};
if (this.isDesktopDevice) {
eBtn.addEventListener('mouseenter', showDropdown);
eVc.addEventListener('mouseleave', hideDropdown);
} else {
eBtn.addEventListener('click', showDropdown);
document.addEventListener('click', handleDocumentClick);
}
eVc.addEventListener('click', hideDropdown);
}
if (videoPlayer && videoBar) {
const eventType = this.isDesktopDevice ? 'mouseenter' : 'click';
videoPlayer.addEventListener(eventType, async () => {
hideVideoMenuBar(videoBarId);
rc.resizeVideoMenuBar();
setCamerasBorderNone();
if (videoBar.classList.contains('hidden')) {
show(videoBar);
animateCSS(videoBar, 'fadeInDown');
if (participantsCount > 1) {
videoPlayer.style.setProperty('border', 'var(--videoBar-active)', 'important');
}
} else {
setCamerasBorderNone();
hide(videoBar);
}
});
if (this.isDesktopDevice) {
videoPlayer.addEventListener('mouseleave', () => {
setCamerasBorderNone();
hideVideoMenuBar('ALL');
});
}
}
}
resizeVideoMenuBar() {
const somethingPinned =
this.isVideoPinned ||
this.isChatPinned ||
this.isEditorPinned ||
this.isPollPinned ||
transcription.isPin();
const menuBarWidth =
this.isVideoPinned || this.isChatPinned || this.isPollPinned || transcription.isPin() ? '75%' : '70%';
const videoMenuBar = rc.getEcN('videoMenuBar');
for (let i = 0; i < videoMenuBar.length; i++) {
const menuBar = videoMenuBar[i];
menuBar.style.width = this.isMobileDevice && somethingPinned ? menuBarWidth : '100%';
}
}
// ####################################################
// REMOVE VIDEO PIN MEDIA CONTAINER
// ####################################################
removeVideoPinMediaContainer() {
this.videoPinMediaContainer.style.display = 'none';
this.videoMediaContainerUnpin();
this.pinnedVideoPlayerId = null;
this.isVideoPinned = false;
if (this.isChatPinned) {
this.chatPin();
}
if (this.isPollPinned) {
this.pollPin();
}
if (this.isEditorPinned) {
this.editorPin();
}
if (this.transcription.isPin()) {
this.transcription.pinned();
}
}
videoMediaContainerPin() {
this.videoMediaContainer.style.top = 0;
this.videoMediaContainer.style.width = '75%';
this.videoMediaContainer.style.height = '100%';
this.resizeVideoMenuBar();
}
videoMediaContainerUnpin() {
this.videoMediaContainer.style.top = 0;
this.videoMediaContainer.style.right = null;
this.videoMediaContainer.style.width = '100%';
this.videoMediaContainer.style.height = '100%';
this.resizeVideoMenuBar();
}
adaptVideoObjectFit(index) {
// 1 (cover) 2 (contain)
BtnVideoObjectFit.selectedIndex = index;
BtnVideoObjectFit.onchange();
}
// ####################################################
// TAKE SNAPSHOT
// ####################################################
handleTS(elemId, tsId) {
let videoPlayer = this.getId(elemId);
let btnTs = this.getId(tsId);
if (btnTs && videoPlayer) {
btnTs.addEventListener('click', () => {
if (videoPlayer.classList.contains('videoCircle')) {
return this.userLog('info', 'SnapShoot not allowed if video on privacy mode', 'top-end');
}
this.sound('snapshot');
let context, canvas, width, height, dataURL;
width = videoPlayer.videoWidth;
height = videoPlayer.videoHeight;
canvas = canvas || document.createElement('canvas');
canvas.width = width;
canvas.height = height;
context = canvas.getContext('2d');
context.drawImage(videoPlayer, 0, 0, width, height);
dataURL = canvas.toDataURL('image/png');
// console.log(dataURL);
saveDataToFile(dataURL, getDataTimeString() + '-SNAPSHOT.png');
});
}
}
// ####################################################
// HANDLE VIDEO MIRROR
// ####################################################
handleMV(elemId, tsId) {
let videoPlayer = this.getId(elemId);
let btnMv = this.getId(tsId);
if (btnMv && videoPlayer) {
btnMv.addEventListener('click', () => {
videoPlayer.classList.toggle('mirror');
});
}
}
// ####################################################
// VIDEO CIRCLE - PRIVACY MODE
// ####################################################
handleVP(elemId, vpId) {
const startVideoInPrivacyMode =
this._moderator.video_start_privacy || localStorageSettings.moderator_video_start_privacy;
let videoPlayer = this.getId(elemId);
let btnVp = this.getId(vpId);
if (btnVp && videoPlayer) {
btnVp.addEventListener('click', () => {
this.sound('click');
this.toggleVideoPrivacyMode();
});
if (startVideoInPrivacyMode) {
btnVp.click();
}
}
}
toggleVideoPrivacyMode() {
isVideoPrivacyActive = !isVideoPrivacyActive;
this.setVideoPrivacyStatus(this.peer_id, isVideoPrivacyActive);
this.emitCmd({
type: 'privacy',
peer_id: this.peer_id,
active: isVideoPrivacyActive,
broadcast: true,
});
}
setVideoPrivacyStatus(elemName, privacy) {
let videoPlayer = this.getName(elemName);
if (!videoPlayer) return;
if (privacy) {
videoPlayer.classList.remove('videoDefault');
videoPlayer.classList.add('videoCircle');
videoPlayer.style.objectFit = 'cover';
} else {
videoPlayer.classList.remove('videoCircle');
videoPlayer.classList.add('videoDefault');
videoPlayer.style.objectFit = 'var(--videoObjFit)';
}
}
// ####################################################
// DRAGGABLE
// ####################################################
makeDraggable(elmnt, dragObj) {
let pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
if (dragObj) {
dragObj.onmousedown = dragMouseDown;
} else {
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
elmnt.style.top = elmnt.offsetTop - pos2 + 'px';
elmnt.style.left = elmnt.offsetLeft - pos1 + 'px';
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
makeUnDraggable(elmnt, dragObj) {
if (dragObj) {
dragObj.onmousedown = null;
} else {
elmnt.onmousedown = null;
}
elmnt.style.top = '';
elmnt.style.left = '';
}
// ####################################################
// CHAT
// ####################################################
handleSM(uid, name) {
const words = uid.split('___');
let peer_id = words[1];
let peer_name = name;
let btnSm = this.getId(uid);
if (btnSm) {
btnSm.addEventListener('click', () => {
this.sendMessageTo(peer_id, peer_name);
});
}
}
isPlistOpen() {
const plist = this.getId('plist');
return !plist.classList.contains('hidden');
}
async toggleChat() {
const chatRoom = this.getId('chatRoom');
chatRoom.classList.toggle('show');
if (!this.isChatOpen) {
await getRoomParticipants();
hide(chatMinButton);
if (!this.isMobileDevice) {
BUTTONS.chat.chatMaxButton && show(chatMaxButton);
}
this.chatCenter();
this.sound('open');
this.showPeerAboutAndMessages(this.chatPeerId, this.chatPeerName, this.chatPeerAvatar);
}
isParticipantsListOpen = !isParticipantsListOpen;
this.isChatOpen = !this.isChatOpen;
if (this.isChatPinned) this.chatUnpin();
if (!this.isMobileDevice && this.isChatOpen && this.canBePinned()) {
this.toggleChatPin();
}
resizeChatRoom();
}
toggleShowParticipants() {
const plist = this.getId('plist');
const chat = this.getId('chat');
plist.classList.toggle('hidden');
const isParticipantsListHidden = !this.isPlistOpen();
chat.style.marginLeft = isParticipantsListHidden ? 0 : '300px';
chat.style.borderLeft = isParticipantsListHidden ? 'none' : '1px solid rgb(255 255 255 / 32%)';
if (this.isChatPinned) elemDisplay(chat.id, isParticipantsListHidden);
if (!this.isChatPinned) elemDisplay(chat.id, true);
this.toggleChatHistorySize(isParticipantsListHidden && (this.isChatPinned || this.isChatMaximized));
plist.style.width = this.isChatPinned || this.isMobileDevice ? '100%' : '300px';
plist.style.position = this.isMobileDevice ? 'fixed' : 'absolute';
}
toggleChatHistorySize(max = true) {
const chatHistory = this.getId('chatHistory');
chatHistory.style.minHeight = max ? 'calc(100vh - 210px)' : '490px';
chatHistory.style.maxHeight = max ? 'calc(100vh - 210px)' : '490px';
}
toggleChatPin() {
if (transcription.isPin()) {
return userLog('info', 'Please unpin the transcription that appears to be currently pinned', 'top-end');
}
if (this.isPollPinned) {
return userLog('info', 'Please unpin the poll that appears to be currently pinned', 'top-end');
}
if (this.isEditorPinned) {
return userLog('info', 'Please unpin the editor that appears to be currently pinned', 'top-end');
}
this.isChatPinned ? this.chatUnpin() : this.chatPin();
this.sound('click');
}
chatMaximize() {
this.isChatMaximized = true;
hide(chatMaxButton);
BUTTONS.chat.chatMaxButton && show(chatMinButton);
this.chatCenter();
document.documentElement.style.setProperty('--msger-width', '100%');
document.documentElement.style.setProperty('--msger-height', '100%');
this.toggleChatHistorySize(true);
}
chatMinimize() {
this.isChatMaximized = false;
hide(chatMinButton);
BUTTONS.chat.chatMaxButton && show(chatMaxButton);
if (this.isChatPinned) {
this.chatPin();
} else {
this.chatCenter();
document.documentElement.style.setProperty('--msger-width', '800px');
document.documentElement.style.setProperty('--msger-height', '700px');
this.toggleChatHistorySize(false);
}
}
canBePinned() {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
return viewportWidth >= 1024 && viewportHeight >= 768;
}
chatPin() {
if (!this.isVideoPinned) {
this.videoMediaContainerPin();
}
this.chatPinned();
this.isChatPinned = true;
setColor(chatTogglePin, 'lime');
this.resizeVideoMenuBar();
resizeVideoMedia();
chatRoom.style.resize = 'none';
if (!this.isMobileDevice) this.makeUnDraggable(chatRoom, chatHeader);
if (this.isPlistOpen()) this.toggleShowParticipants();
if (chatRoom.classList.contains('container')) chatRoom.classList.remove('container');
}
chatUnpin() {
if (!this.isVideoPinned) {
this.videoMediaContainerUnpin();
}
document.documentElement.style.setProperty('--msger-width', '800px');
document.documentElement.style.setProperty('--msger-height', '700px');
hide(chatMinButton);
BUTTONS.chat.chatMaxButton && show(chatMaxButton);
this.chatCenter();
this.isChatPinned = false;
setColor(chatTogglePin, 'white');
this.resizeVideoMenuBar();
resizeVideoMedia();
if (!this.isMobileDevice) this.makeDraggable(chatRoom, chatHeader);
if (!this.isPlistOpen()) this.toggleShowParticipants();
if (!chatRoom.classList.contains('container')) chatRoom.classList.add('container');
resizeChatRoom();
}
chatCenter() {
chatRoom.style.position = 'fixed';
chatRoom.style.transform = 'translate(-50%, -50%)';
chatRoom.style.top = '50%';
chatRoom.style.left = '50%';
}
chatPinned() {
chatRoom.style.position = 'absolute';
chatRoom.style.top = 0;
chatRoom.style.right = 0;
chatRoom.style.left = null;
chatRoom.style.transform = null;
document.documentElement.style.setProperty('--msger-width', '25%');
document.documentElement.style.setProperty('--msger-height', '100%');
}
toggleChatEmoji() {
this.getId('chatEmoji').classList.toggle('show');
this.isChatEmojiOpen = this.isChatEmojiOpen ? false : true;
this.getId('chatEmojiButton').style.color = this.isChatEmojiOpen ? '#FFFF00' : '#FFFFFF';
}
addEmojiToMsg(data) {
msgerInput.value += data.native;
toggleChatEmoji();
}
cleanMessage() {
chatMessage.value = '';
chatMessage.setAttribute('rows', '1');
}
pasteMessage() {
navigator.clipboard
.readText()
.then((text) => {
chatMessage.value += text;
isChatPasteTxt = true;
this.checkLineBreaks();
})
.catch((err) => {
console.error('Failed to read clipboard contents: ', err);
});
}
sendMessage() {
if (!this.thereAreParticipants() && !isChatGPTOn && !isDeepSeekOn) {
this.cleanMessage();
isChatPasteTxt = false;
return this.userLog('info', 'No participants in the room', 'top-end');
}
// Prevent long messages
if (this.chatMessageLengthCheck && chatMessage.value.length > this.chatMessageLength) {
return this.userLog(
'warning',
`The message seems too long, with a maximum of ${this.chatMessageLength} characters allowed`,
'top-end'
);
}
// Spamming detected ban the user from the room
if (this.chatMessageSpamCount == this.chatMessageSpamCountToBan) {
return this.roomAction('isBanned', true);
}
// Prevent Spam messages
const currentTime = Date.now();
if (chatMessage.value && currentTime - this.chatMessageTimeLast <= this.chatMessageTimeBetween) {
this.cleanMessage();
chatMessage.readOnly = true;
chatSendButton.disabled = true;
setTimeout(function () {
chatMessage.readOnly = false;
chatSendButton.disabled = false;
}, this.chatMessageNotifyDelay);
this.chatMessageSpamCount++;
return this.userLog(
'warning',
`Kindly refrain from spamming. Please wait ${this.chatMessageNotifyDelay / 1000} seconds before sending another message`,
'top-end',
this.chatMessageNotifyDelay
);
}
this.chatMessageTimeLast = currentTime;
chatMessage.value = filterXSS(chatMessage.value.trim());
const peer_msg = this.formatMsg(chatMessage.value);
if (!peer_msg) {
return this.cleanMessage();
}
this.peer_name = filterXSS(this.peer_name);
const data = {
room_id: this.room_id,
peer_name: this.peer_name,
peer_avatar: this.peer_avatar,
peer_id: this.peer_id,
to_peer_id: '',
to_peer_name: '',
peer_msg: peer_msg,
};
if (isChatGPTOn) {
data.to_peer_id = 'ChatGPT';
data.to_peer_name = 'ChatGPT';
console.log('Send message:', data);
this.socket.emit('message', data);
this.setMsgAvatar('left', this.peer_name, this.peer_avatar);
this.appendMessage(
'left',
this.leftMsgAvatar,
this.peer_name,
this.peer_id,
peer_msg,
data.to_peer_id,
data.to_peer_name
);
this.cleanMessage();
this.socket
.request('getChatGPT', {
time: getDataTimeString(),
room: this.room_id,
name: this.peer_name,
prompt: peer_msg,
context: this.chatGPTContext,
})
.then((completion) => {
if (!completion) return;
const { message, context } = completion;
this.chatGPTContext = context ? context : [];
console.log('Receive message:', message);
this.setMsgAvatar('right', 'ChatGPT');
this.appendMessage('right', image.chatgpt, 'ChatGPT', this.peer_id, message, 'ChatGPT', 'ChatGPT');
this.cleanMessage();
this.streamingTask(message); // Video AI avatar speak
this.speechInMessages && !VideoAI.active
? this.speechMessage(true, 'ChatGPT', message)
: this.sound('message');
})
.catch((err) => {
console.log('ChatGPT error:', err);
});
}
if (isDeepSeekOn) {
data.to_peer_id = 'DeepSeek';
data.to_peer_name = 'DeepSeek';
console.log('Send message:', data);
this.socket.emit('message', data);
this.setMsgAvatar('left', this.peer_name, this.peer_avatar);
this.appendMessage(
'left',
this.leftMsgAvatar,
this.peer_name,
this.peer_id,
peer_msg,
data.to_peer_id,
data.to_peer_name
);
this.cleanMessage();
this.socket
.request('getDeepSeek', {
time: getDataTimeString(),
room: this.room_id,
name: this.peer_name,
prompt: peer_msg,
context: this.deepSeekContext,
})
.then((completion) => {
if (!completion) return;
const { message, context } = completion;
this.deepSeekContext = context ? context : [];
console.log('Receive message:', message);
this.setMsgAvatar('right', 'DeepSeek');
this.appendMessage(
'right',
image.deepSeek,
'DeepSeek',
this.peer_id,
message,
'DeepSeek',
'DeepSeek'
);
this.cleanMessage();
this.streamingTask(message);
this.speechInMessages && !VideoAI.active
? this.speechMessage(true, 'DeepSeek', message)
: this.sound('message');
})
.catch((err) => {
console.log('DeepSeek error:', err);
});
}
if (!isChatGPTOn && !isDeepSeekOn) {
const participantsList = this.getId('participantsList');
const participantsListItems = participantsList.getElementsByTagName('li');
for (let i = 0; i < participantsListItems.length; i++) {
const li = participantsListItems[i];
if (li.classList.contains('active')) {
data.to_peer_id = li.getAttribute('data-to-id');
data.to_peer_name = li.getAttribute('data-to-name');
console.log('Send message:', data);
this.socket.emit('message', data);
this.setMsgAvatar('left', this.peer_name, this.peer_avatar);
this.appendMessage(
'left',
this.leftMsgAvatar,
this.peer_name,
this.peer_id,
peer_msg,
data.to_peer_id,
data.to_peer_name
);
this.cleanMessage();
}
}
}
}
sendMessageTo(to_peer_id, to_peer_name) {
if (!this.thereAreParticipants()) {
isChatPasteTxt = false;
this.cleanMessage();
return this.userLog('info', 'No participants in the room except you', 'top-end');
}
Swal.fire({
background: swalBackground,
position: 'center',
imageUrl: image.message,
input: 'text',
inputPlaceholder: '💬 Enter your message...',
showCancelButton: true,
confirmButtonText: `Send`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.value) {
result.value = filterXSS(result.value.trim());
let peer_msg = this.formatMsg(result.value);
if (!peer_msg) {
return this.cleanMessage();
}
this.peer_name = filterXSS(this.peer_name);
const toPeerName = filterXSS(to_peer_name);
let data = {
peer_name: this.peer_name,
peer_avatar: this.peer_avatar,
peer_id: this.peer_id,
to_peer_id: to_peer_id,
to_peer_name: toPeerName,
peer_msg: peer_msg,
};
console.log('Send message:', data);
this.socket.emit('message', data);
this.setMsgAvatar('left', this.peer_name, this.peer_avatar);
this.appendMessage(
'left',
this.leftMsgAvatar,
this.peer_name,
this.peer_id,
peer_msg,
to_peer_id,
toPeerName
);
if (!this.isChatOpen) this.toggleChat();
}
});
}
async showMessage(data, toggleChat = true) {
if (toggleChat && !this.isChatOpen && this.showChatOnMessage) {
await this.toggleChat();
}
this.setMsgAvatar('right', data.peer_name, data.peer_avatar);
this.appendMessage(
'right',
this.rightMsgAvatar,
data.peer_name,
data.peer_id,
data.peer_msg,
data.to_peer_id,
data.to_peer_name
);
if (!this.showChatOnMessage) {
this.userLog('info', `💬 New message from: ${data.peer_name}`, 'top-end');
}
if (this.speechInMessages) {
VideoAI.active
? this.streamingTask(`New message from: ${data.peer_name}, the message is: ${data.peer_msg}`)
: this.speechMessage(true, data.peer_name, data.peer_msg);
} else {
this.sound('message');
}
const participantsList = this.getId('participantsList');
const participantsListItems = participantsList.getElementsByTagName('li');
for (let i = 0; i < participantsListItems.length; i++) {
const li = participantsListItems[i];
// INCOMING PRIVATE MESSAGE
if (li.id === data.peer_id && data.to_peer_id != 'all') {
li.classList.add('pulsate');
if (!['all', 'ChatGPT', 'DeepSeek'].includes(data.to_peer_id)) {
this.getId(`${data.peer_id}-unread-msg`).classList.remove('hidden');
}
}
}
}
setMsgAvatar(avatar, peerName, peerAvatar = false) {
const avatarImg =
peerAvatar && this.isImageURL(peerAvatar)
? peerAvatar
: this.isValidEmail(peerName)
? this.genGravatar(peerName)
: this.genAvatarSvg(peerName, 32);
avatar === 'left' ? (this.leftMsgAvatar = avatarImg) : (this.rightMsgAvatar = avatarImg);
}
appendMessage(side, img, fromName, fromId, msg, toId, toName) {
const getSide = filterXSS(side);
const getImg = filterXSS(img);
const getFromName = filterXSS(fromName);
const getFromId = filterXSS(fromId);
const getMsg = filterXSS(msg);
const getToId = filterXSS(toId);
const getToName = filterXSS(toName);
const time = this.getTimeNow();
const myMessage = getSide === 'left';
const messageClass = myMessage ? 'my-message' : 'other-message float-right';
const messageData = myMessage ? 'text-start' : 'text-end';
const timeAndName = myMessage
? ``
: ``;
const formatMessage = this.formatMsg(getMsg);
const speechButton = this.isSpeechSynthesisSupported
? ``
: '';
const positionFirst = myMessage
? `${timeAndName}`
: `${timeAndName}
`;
const newMessageHTML = `
${part.value}`;
}
})
.join('');
}
deleteMessage(id) {
Swal.fire({
background: swalBackground,
position: 'center',
title: 'Delete this Message?',
imageUrl: image.delete,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
this.getId(id).remove();
this.sound('delete');
}
});
}
copyToClipboard(id) {
const text = this.getId(id).innerText;
navigator.clipboard
.writeText(text)
.then(() => {
this.userLog('success', 'Message copied!', 'top-end', 1000);
})
.catch((err) => {
this.userLog('error', err, 'top-end', 6000);
});
}
formatMsg(msg) {
const message = filterXSS(msg);
if (message.trim().length == 0) return;
if (this.isHtml(message)) return this.sanitizeHtml(message);
if (this.isValidHttpURL(message)) {
if (this.isImageURL(message)) return this.getImage(message);
//if (this.isVideoTypeSupported(message)) return this.getIframe(message);
return this.getLink(message);
}
if (isChatMarkdownOn) return marked.parse(message);
if (isChatPasteTxt && this.getLineBreaks(message) > 1) {
isChatPasteTxt = false;
return this.getPre(message);
}
if (this.getLineBreaks(message) > 1) return this.getPre(message);
console.log('FormatMsg', message);
return message;
}
sanitizeHtml(input) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
return input.replace(/[&<>"'/]/g, (m) => map[m]);
}
isHtml(str) {
const a = document.createElement('div');
a.innerHTML = str;
for (var c = a.childNodes, i = c.length; i--; ) {
if (c[i].nodeType == 1) return true;
}
return false;
}
isValidHttpURL(input) {
try {
new URL(input);
return true;
} catch (_) {
return false;
}
}
isImageURL(input) {
if (!input || typeof input !== 'string') return false;
try {
const url = new URL(input);
return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.svg'].some((ext) =>
url.pathname.toLowerCase().endsWith(ext)
);
} catch (e) {
return false;
}
}
getImage(input) {
const url = filterXSS(input);
const div = document.createElement('div');
const img = document.createElement('img');
img.setAttribute('src', url);
img.setAttribute('width', '200px');
img.setAttribute('height', 'auto');
div.appendChild(img);
console.log('GetImg', div.firstChild.outerHTML);
return div.firstChild.outerHTML;
}
getLink(input) {
const url = filterXSS(input);
const a = document.createElement('a');
const div = document.createElement('div');
const linkText = document.createTextNode(url);
a.setAttribute('href', url);
a.setAttribute('target', '_blank');
a.appendChild(linkText);
div.appendChild(a);
console.log('GetLink', div.firstChild.outerHTML);
return div.firstChild.outerHTML;
}
getPre(input) {
const text = filterXSS(input);
const pre = document.createElement('pre');
const div = document.createElement('div');
pre.textContent = text;
div.appendChild(pre);
console.log('GetPre', div.firstChild.outerHTML);
return div.firstChild.outerHTML;
}
getIframe(input) {
const url = filterXSS(input);
const iframe = document.createElement('iframe');
const div = document.createElement('div');
const is_youtube = this.getVideoType(url) == 'na' ? true : false;
const video_audio_url = is_youtube ? this.getYoutubeEmbed(url) : url;
iframe.setAttribute('title', 'Chat-IFrame');
iframe.setAttribute('src', video_audio_url);
iframe.setAttribute('width', 'auto');
iframe.setAttribute('frameborder', '0');
iframe.setAttribute(
'allow',
'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
);
iframe.setAttribute('allowfullscreen', 'allowfullscreen');
div.appendChild(iframe);
console.log('GetIFrame', div.firstChild.outerHTML);
return div.firstChild.outerHTML;
}
getLineBreaks(message) {
return (message.match(/\n/g) || []).length;
}
checkLineBreaks() {
chatMessage.style.height = '';
if (this.getLineBreaks(chatMessage.value) > 0 || chatMessage.value.length > 50) {
chatMessage.setAttribute('rows', '2');
}
}
collectMessages(time, from, msg) {
this.chatMessages.push({
time: time,
from: from,
msg: msg,
});
}
speechMessage(newMsg = true, from, msg) {
const speech = new SpeechSynthesisUtterance();
speech.text = (newMsg ? 'New' : '') + ' message from:' + from + '. The message is:' + msg;
speech.rate = 0.9;
window.speechSynthesis.speak(speech);
}
speechElementText(elemId) {
const element = this.getId(elemId);
this.speechText(element.innerText);
}
speechText(msg) {
if (VideoAI.active) {
this.streamingTask(msg);
} else {
const speech = new SpeechSynthesisUtterance();
speech.text = msg;
speech.rate = 0.9;
window.speechSynthesis.speak(speech);
}
}
chatToggleBg() {
this.isChatBgTransparent = !this.isChatBgTransparent;
this.isChatBgTransparent
? document.documentElement.style.setProperty('--msger-bg', 'rgba(0, 0, 0, 0.100)')
: setTheme();
}
chatClean() {
if (this.chatMessages.length === 0) {
return userLog('info', 'No chat messages to clean', 'top-end');
}
Swal.fire({
background: swalBackground,
position: 'center',
title: 'Clean up all chat Messages?',
imageUrl: image.delete,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
function removeAllChildNodes(parentNode) {
while (parentNode.firstChild) {
parentNode.removeChild(parentNode.firstChild);
}
}
// Remove child nodes from different message containers
removeAllChildNodes(chatGPTMessages);
removeAllChildNodes(deepSeekMessages);
removeAllChildNodes(chatPublicMessages);
removeAllChildNodes(chatPrivateMessages);
this.chatMessages = [];
this.chatGPTContext = [];
this.deepSeekContext = [];
this.sound('delete');
}
});
}
chatSave() {
if (this.chatMessages.length === 0) {
return userLog('info', 'No chat messages to save', 'top-end');
}
saveObjToJsonFile(this.chatMessages, 'CHAT');
}
// ##############################################
// POOLS
// ##############################################
togglePoll() {
pollRoom.classList.toggle('show');
if (!this.isPollOpen) {
hide(pollMinButton);
if (!this.isMobileDevice) {
BUTTONS.poll.pollMaxButton && show(pollMaxButton);
}
this.pollCenter();
this.sound('open');
}
this.isPollOpen = !this.isPollOpen;
if (this.isPollPinned) this.pollUnpin();
if (!this.isMobileDevice && this.isPollOpen && this.canBePinned()) {
this.togglePollPin();
}
}
togglePollPin() {
if (transcription.isPin()) {
return userLog('info', 'Please unpin the transcription that appears to be currently pinned', 'top-end');
}
if (this.isChatPinned) {
return userLog('info', 'Please unpin the chat that appears to be currently pinned', 'top-end');
}
if (this.isEditorPinned) {
return userLog('info', 'Please unpin the editor that appears to be currently pinned', 'top-end');
}
this.isPollPinned ? this.pollUnpin() : this.pollPin();
this.sound('click');
}
pollPin() {
if (!this.isVideoPinned) {
this.videoMediaContainerPin();
}
this.pollPinned();
this.isPollPinned = true;
setColor(pollTogglePin, 'lime');
this.resizeVideoMenuBar();
resizeVideoMedia();
pollRoom.style.resize = 'none';
if (!this.isMobileDevice) this.makeUnDraggable(pollRoom, pollHeader);
}
pollUnpin() {
if (!this.isVideoPinned) {
this.videoMediaContainerUnpin();
}
pollRoom.style.maxWidth = '600px';
pollRoom.style.maxHeight = '700px';
this.pollCenter();
this.isPollPinned = false;
setColor(pollTogglePin, 'white');
this.resizeVideoMenuBar();
resizeVideoMedia();
if (!this.isMobileDevice) this.makeDraggable(pollRoom, pollHeader);
}
pollPinned() {
pollRoom.style.position = 'absolute';
pollRoom.style.top = 0;
pollRoom.style.right = 0;
pollRoom.style.left = null;
pollRoom.style.transform = null;
pollRoom.style.maxWidth = '25%';
pollRoom.style.maxHeight = '100%';
}
pollCenter() {
pollRoom.style.position = 'fixed';
pollRoom.style.transform = 'translate(-50%, -50%)';
pollRoom.style.top = '50%';
pollRoom.style.left = '50%';
}
pollMaximize() {
pollRoom.style.maxHeight = '100vh';
pollRoom.style.maxWidth = '100vw';
this.pollCenter();
hide(pollMaxButton);
BUTTONS.poll.pollMaxButton && show(pollMinButton);
}
pollMinimize() {
this.pollCenter();
hide(pollMinButton);
BUTTONS.poll.pollMaxButton && show(pollMaxButton);
if (this.isPollPinned) {
this.pollPin();
} else {
pollRoom.style.maxWidth = '600px';
pollRoom.style.maxHeight = '700px';
}
}
pollsUpdate(polls) {
if (!this.isPollOpen) this.togglePoll();
pollsContainer.innerHTML = '';
polls.forEach((poll, index) => {
const pollDiv = document.createElement('div');
pollDiv.className = 'poll';
const question = document.createElement('p');
question.className = 'poll-question';
question.textContent = poll.question;
pollDiv.appendChild(question);
const options = document.createElement('div');
options.className = 'options';
poll.options.forEach((option) => {
const optionDiv = document.createElement('div');
const input = document.createElement('input');
input.type = 'radio';
input.name = `poll${index}`;
input.value = option;
if (this.pollSelectedOptions[index] === option) {
input.checked = true;
}
input.addEventListener('change', () => {
this.pollSelectedOptions[index] = option;
this.socket.emit('vote', { pollIndex: index, option });
});
const label = document.createElement('label');
label.textContent = option;
optionDiv.appendChild(input);
optionDiv.appendChild(label);
options.appendChild(optionDiv);
});
pollDiv.appendChild(options);
// Only the presenters
// if (isPresenter) {
const pollButtonsDiv = document.createElement('div');
pollButtonsDiv.className = 'poll-btns';
// Toggle voters button
const toggleButton = document.createElement('button');
const toggleButtonIcon = document.createElement('i');
toggleButtonIcon.className = 'fas fa-users';
toggleButton.id = 'toggleVoters';
toggleButton.className = 'view-btn';
// Append the icon to the button
toggleButton.insertBefore(toggleButtonIcon, toggleButton.firstChild);
toggleButton.addEventListener('click', () => {
votersList.style.display === 'none'
? (votersList.style.display = 'block')
: (votersList.style.display = 'none');
});
pollButtonsDiv.appendChild(toggleButton);
// Edit poll button using swal
const editPollButton = document.createElement('button');
const editPollButtonIcon = document.createElement('i');
editPollButtonIcon.className = 'fas fa-pen-to-square';
editPollButton.id = 'editPoll';
editPollButton.className = 'poll-btn';
editPollButton.insertBefore(editPollButtonIcon, editPollButton.firstChild);
editPollButton.addEventListener('click', () => {
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
title: 'Edit Poll',
html: this.createPollInputs(poll),
focusConfirm: false,
showCancelButton: true,
confirmButtonText: 'Save',
cancelButtonText: 'Cancel',
cancelButtonColor: '#dc3545',
preConfirm: () => {
const newQuestion = document.getElementById('swal-input-question').value;
const newOptions = this.getPollOptions(poll.options.length);
this.socket.emit('editPoll', {
index,
question: newQuestion,
options: newOptions,
peer_name: this.peer_name,
peer_uuid: this.peer_uuid,
});
},
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
});
pollButtonsDiv.appendChild(editPollButton);
// Delete poll button
const deletePollButton = document.createElement('button');
const deletePollButtonIcon = document.createElement('i');
deletePollButtonIcon.className = 'fas fa-trash';
deletePollButton.id = 'delPoll';
deletePollButton.className = 'del-btn';
deletePollButton.insertBefore(deletePollButtonIcon, deletePollButton.firstChild);
deletePollButton.addEventListener('click', () => {
// confirm before delete poll
Swal.fire({
background: swalBackground,
position: 'top',
title: 'Delete this poll?',
imageUrl: image.delete,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
this.socket.emit('deletePoll', { index, peer_name: this.peer_name, peer_uuid: this.peer_uuid });
}
});
});
pollButtonsDiv.appendChild(deletePollButton);
// Add thematic break
const hr = document.createElement('hr');
pollDiv.appendChild(hr);
// Append buttons to poll
pollDiv.appendChild(pollButtonsDiv);
// Create voter lists
const votersList = document.createElement('ul');
votersList.style.display = 'none';
for (const [user, vote] of Object.entries(poll.voters)) {
const voter = document.createElement('li');
voter.textContent = `${user}: ${vote}`;
votersList.appendChild(voter);
}
pollDiv.appendChild(votersList);
// }
pollsContainer.appendChild(pollDiv);
if (!this.isMobileDevice) {
setTippy('toggleVoters', 'Toggle voters', 'top');
setTippy('delPoll', 'Delete poll', 'top');
setTippy('editPoll', 'Edit poll', 'top');
}
});
}
pollCreateNewForm(e) {
e.preventDefault();
const question = e.target.question.value;
const optionInputs = document.querySelectorAll('.option-input');
const options = Array.from(optionInputs).map((input) => input.value.trim());
this.socket.emit('createPoll', { question, options });
e.target.reset();
optionsContainer.innerHTML = '';
const initialOptionInput = document.createElement('input');
initialOptionInput.type = 'text';
initialOptionInput.name = 'option';
initialOptionInput.className = 'option-input';
initialOptionInput.required = true;
optionsContainer.appendChild(initialOptionInput);
}
pollAddOptions() {
const optionInput = document.createElement('input');
optionInput.type = 'text';
optionInput.name = 'option';
optionInput.className = 'option-input';
optionInput.required = true;
optionsContainer.appendChild(optionInput);
}
pollDeleteOptions() {
const optionInputs = document.querySelectorAll('.option-input');
if (optionInputs.length > 1) {
optionsContainer.removeChild(optionInputs[optionInputs.length - 1]);
}
}
createPollInputs(poll) {
const questionInput = ``;
const optionsInputs = poll.options
.map((option, i) => ``)
.join('');
return questionInput + optionsInputs;
}
getPollOptions(optionCount) {
const options = [];
for (let i = 0; i < optionCount; i++) {
options.push(document.getElementById(`swal-input-option${i}`).value);
}
return options;
}
pollSaveResults() {
const polls = document.querySelectorAll('.poll');
const results = [];
polls.forEach((poll, index) => {
const question = poll.querySelector('.poll-question').textContent;
const options = poll.querySelectorAll('.options div label');
const optionsText = Array.from(options).reduce((acc, option, index) => {
acc[index + 1] = option.textContent.trim();
return acc;
}, {});
const votersList = poll.querySelector('ul');
const voters = Array.from(votersList.querySelectorAll('li')).reduce((acc, li) => {
const [name, vote] = li.textContent.split(':').map((item) => item.trim());
acc[name] = vote;
return acc;
}, {});
results.push({
Poll: `${index + 1}`,
question: question,
options: optionsText,
voters: voters,
});
});
results.length > 0
? saveObjToJsonFile(results, 'Poll')
: this.userLog('info', 'No polling data available to save', 'top-end');
}
getPollFileName() {
const dateTime = getDataTimeStringFormat();
const roomName = this.room_id.trim();
return `Poll_${roomName}_${dateTime}.txt`;
}
// ####################################################
// EDITOR
// ####################################################
toggleEditor() {
editorRoom.classList.toggle('show');
if (!this.isEditorOpen) {
this.editorCenter();
this.sound('open');
}
this.isEditorOpen = !this.isEditorOpen;
if (this.isEditorPinned) this.editorUnpin();
if (!this.isMobileDevice && this.isEditorOpen && this.canBePinned()) {
this.toggleEditorPin();
}
}
toggleLockUnlockEditor() {
this.isEditorLocked = !this.isEditorLocked;
const btnToShow = this.isEditorLocked ? editorLockBtn : editorUnlockBtn;
const btnToHide = this.isEditorLocked ? editorUnlockBtn : editorLockBtn;
const btnColor = this.isEditorLocked ? 'red' : 'white';
const action = this.isEditorLocked ? 'lock' : 'unlock';
show(btnToShow);
hide(btnToHide);
setColor(editorLockBtn, btnColor);
this.editorSendAction(action);
if (this.isEditorLocked) {
userLog('info', 'The Editor is locked. \n The participants cannot interact with it.', 'top-right');
sound('locked');
}
}
editorCenter() {
editorRoom.style.position = 'fixed';
editorRoom.style.transform = 'translate(-50%, -50%)';
editorRoom.style.top = '50%';
editorRoom.style.left = '50%';
}
toggleEditorPin() {
if (transcription.isPin()) {
return userLog('info', 'Please unpin the transcription that appears to be currently pinned', 'top-end');
}
if (this.isPollPinned) {
return userLog('info', 'Please unpin the poll that appears to be currently pinned', 'top-end');
}
if (this.isChatPinned) {
return userLog('info', 'Please unpin the chat that appears to be currently pinned', 'top-end');
}
this.isEditorPinned ? this.editorUnpin() : this.editorPin();
this.sound('click');
}
editorPin() {
if (!this.isVideoPinned) {
this.videoMediaContainer.style.top = 0;
this.videoMediaContainer.style.width = '70%';
this.videoMediaContainer.style.height = '100%';
}
this.editorPinned();
this.isEditorPinned = true;
setColor(editorTogglePin, 'lime');
this.resizeVideoMenuBar();
resizeVideoMedia();
document.documentElement.style.setProperty('--editor-height', '80vh');
//if (!this.isMobileDevice) this.makeUnDraggable(editorRoom, editorHeader);
}
editorUnpin() {
if (!this.isVideoPinned) {
this.videoMediaContainerUnpin();
}
editorRoom.style.maxWidth = '100%';
editorRoom.style.maxHeight = '100%';
this.pollCenter();
this.isEditorPinned = false;
setColor(editorTogglePin, 'white');
this.resizeVideoMenuBar();
resizeVideoMedia();
document.documentElement.style.setProperty('--editor-height', '85vh');
//if (!this.isMobileDevice) this.makeDraggable(editorRoom, editorHeader);
}
editorPinned() {
editorRoom.style.position = 'absolute';
editorRoom.style.top = 0;
editorRoom.style.right = 0;
editorRoom.style.left = null;
editorRoom.style.transform = null;
editorRoom.style.maxWidth = '30%';
editorRoom.style.maxHeight = '100%';
}
editorUpdate() {
if (this.isEditorOpen && (!isRulesActive || isPresenter)) {
console.log('IsPresenter: update editor content to the participants in the room');
const content = quill.getContents(); // Get content in Delta format
this.socket.emit('editorUpdate', content);
const action = this.isEditorLocked ? 'lock' : 'unlock';
this.editorSendAction(action);
}
}
handleEditorUpdateData(data) {
this.editorOpen();
quill.setContents(data);
}
handleEditorData(data) {
this.editorOpen();
quill.updateContents(data);
}
editorOpen() {
if (!this.isEditorOpen) {
this.sound('open');
this.toggleEditor();
}
}
handleEditorActionsData(data) {
const { peer_name, action } = data;
switch (action) {
case 'open':
if (this.isEditorOpen) return;
this.toggleEditor();
this.userLog('info', `${icons.editor} ${peer_name} open editor`, 'top-end', 6000);
break;
case 'close':
if (!this.isEditorOpen) return;
this.toggleEditor();
this.userLog('info', `${icons.editor} ${peer_name} close editor`, 'top-end', 6000);
break;
case 'clean':
quill.setText('');
this.userLog('info', `${icons.editor} ${peer_name} cleared editor`, 'top-end', 6000);
break;
case 'lock':
this.isEditorLocked = true;
quill.enable(false);
this.userLog('info', `${icons.editor} ${peer_name} locked the editor`, 'top-end', 6000);
break;
case 'unlock':
this.isEditorLocked = false;
quill.enable(true);
this.userLog('info', `${icons.editor} ${peer_name} unlocked the editor`, 'top-end', 6000);
break;
default:
break;
}
}
editorIsLocked() {
return this.isEditorLocked;
}
editorUndo() {
quill.history.undo();
}
editorRedo() {
quill.history.redo();
}
editorCopy() {
const content = quill.getText();
if (content.trim().length === 0) {
return this.userLog('info', 'Nothing to copy', 'top-end');
}
copyToClipboard(content, false);
}
editorClean() {
if (!isPresenter && this.editorIsLocked()) {
userLog('info', 'The Editor is locked. \n You cannot interact with it.', 'top-right');
return;
}
const content = quill.getText();
if (content.trim().length === 0) {
return this.userLog('info', 'Nothing to clear', 'top-end');
}
Swal.fire({
background: swalBackground,
position: 'center',
title: 'Clear the editor content?',
imageUrl: image.delete,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
quill.setText('');
this.editorSendAction('clean');
this.sound('delete');
}
});
}
editorSave() {
Swal.fire({
background: swalBackground,
position: 'top',
imageUrl: image.save,
title: 'Editor save options',
showDenyButton: true,
showCancelButton: true,
cancelButtonColor: 'red',
denyButtonColor: 'green',
confirmButtonText: `Text`,
denyButtonText: `Html`,
cancelButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
this.handleEditorSaveResult(result);
});
}
handleEditorSaveResult(result) {
if (result.isConfirmed) {
this.saveEditorAsText();
} else if (result.isDenied) {
this.saveEditorAsHtml();
}
}
saveEditorAsText() {
const content = quill.getText().trim();
if (content.length === 0) {
return this.userLog('info', 'No data to save!', 'top-end');
}
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const fileName = this.generateFileName('editor.txt');
this.saveBlobToFile(blob, fileName);
this.sound('download');
}
saveEditorAsHtml() {
const content = quill.root.innerHTML.trim();
if (content === 'Drag and drop your file here
' + JSON.stringify(obj, null, 4) + ''; } isValidFileName(fileName) { const invalidChars = /[\\\/\?\*\|:"<>]/; return !invalidChars.test(fileName); } // #################################################### // SHARE VIDEO YOUTUBE - MP4 - WEBM - OGG or AUDIO mp3 // #################################################### handleSV(uid) { const words = uid.split('___'); let peer_id = words[1]; let btnSv = this.getId(uid); if (btnSv) { btnSv.addEventListener('click', () => { this.shareVideo(peer_id); }); } } shareVideo(peer_id = 'all') { if (this._moderator.media_cant_sharing) { return userLog('warning', 'The moderator does not allow you to share any media', 'top-end', 6000); } this.sound('open'); Swal.fire({ background: swalBackground, position: 'center', imageUrl: image.videoShare, title: 'Share a Video or Audio', text: 'Paste a Video or Audio URL', input: 'text', showCancelButton: true, confirmButtonText: `Share`, showClass: { popup: 'animate__animated animate__fadeInDown' }, hideClass: { popup: 'animate__animated animate__fadeOutUp' }, }).then((result) => { if (result.value) { result.value = filterXSS(result.value); // if (!this.thereAreParticipants()) { // return userLog('info', 'No participants detected', 'top-end'); // } if (!this.isVideoTypeSupported(result.value)) { return userLog('warning', 'Something wrong, try with another Video or audio URL'); } /* https://www.youtube.com/watch?v=RT6_Id5-7-s https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3 */ let is_youtube = this.getVideoType(result.value) == 'na' ? true : false; let video_url = is_youtube ? this.getYoutubeEmbed(result.value) : result.value; if (video_url) { let data = { peer_id: peer_id, peer_name: this.peer_name, video_url: video_url, is_youtube: is_youtube, action: 'open', }; console.log('Video URL: ', video_url); this.socket.emit('shareVideoAction', data); this.openVideo(data); } else { this.userLog('error', 'Not valid video URL', 'top-end', 6000); } } }); // Take URL from clipboard ex: // https://www.youtube.com/watch?v=1ZYbU82GVz4 navigator.clipboard .readText() .then((clipboardText) => { if (!clipboardText) return false; const sanitizedText = filterXSS(clipboardText); const inputElement = Swal.getInput(); if (this.isVideoTypeSupported(sanitizedText) && inputElement) { inputElement.value = sanitizedText; } return false; }) .catch(() => { return false; }); } getVideoType(url) { if (url.endsWith('.mp4')) return 'video/mp4'; if (url.endsWith('.mp3')) return 'video/mp3'; if (url.endsWith('.webm')) return 'video/webm'; if (url.endsWith('.ogg')) return 'video/ogg'; return 'na'; } isVideoTypeSupported(url) { if ( url.endsWith('.mp4') || url.endsWith('.mp3') || url.endsWith('.webm') || url.endsWith('.ogg') || url.includes('youtube.com') ) return true; return false; } getYoutubeEmbed(url) { let regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; let match = url.match(regExp); return match && match[7].length == 11 ? 'https://www.youtube.com/embed/' + match[7] + '?autoplay=1' : false; } shareVideoAction(data) { const { peer_name, action } = data; switch (action) { case 'open': this.userLog('info', `${peer_name} opened the video`, 'top-end'); this.openVideo(data); break; case 'close': this.userLog('info', `${peer_name} closed the video`, 'top-end'); this.closeVideo(); break; default: break; } } openVideo(data) { let d, vb, e, video, pn, fsBtn; let peer_name = data.peer_name; let video_url = data.video_url + (this.isMobileSafari ? '&enablejsapi=1&mute=1' : ''); // Safari need user interaction let is_youtube = data.is_youtube; let video_type = this.getVideoType(video_url); this.closeVideo(); show(videoCloseBtn); d = document.createElement('div'); d.className = 'Camera'; d.id = '__shareVideo'; vb = document.createElement('div'); vb.setAttribute('id', '__videoBar'); vb.className = 'videoMenuBarShare fadein'; e = this.createButton('__videoExit', 'fas fa-times'); pn = this.createButton('__pinUnpin', html.pin); fsBtn = this.createButton('__videoFS', html.fullScreen); if (is_youtube) { video = document.createElement('iframe'); video.setAttribute('title', peer_name); video.setAttribute( 'allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' ); video.setAttribute('frameborder', '0'); video.setAttribute('allowfullscreen', true); // Safari on Mobile needs user interaction to unmute video if (this.isMobileSafari) { Swal.fire({ allowOutsideClick: false, allowEscapeKey: false, background: swalBackground, position: 'top', imageUrl: image.videoShare, title: 'Unmute Video', text: 'Tap the button below to unmute and play the video with sound.', confirmButtonText: 'Unmute', didOpen: () => { const unmuteButton = Swal.getConfirmButton(); if (unmuteButton) unmuteButton.focus(); }, }).then((result) => { if (result.isConfirmed) { if (video && video.contentWindow) { video.contentWindow.postMessage('{"event":"command","func":"unMute","args":""}', '*'); video.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); } } }); } } else { video = document.createElement('video'); video.type = video_type; video.autoplay = true; video.controls = true; if (video_type == 'video/mp3') { video.poster = image.audio; } } video.setAttribute('id', '__videoShare'); video.setAttribute('src', video_url); video.setAttribute('width', '100%'); video.setAttribute('height', '100%'); vb.appendChild(e); vb.appendChild(fsBtn); if (!this.isMobileDevice) vb.appendChild(pn); d.appendChild(video); d.appendChild(vb); this.videoMediaContainer.appendChild(d); fsBtn.addEventListener('click', () => { // Try to use the Fullscreen API if ( video.requestFullscreen || video.webkitRequestFullscreen || video.mozRequestFullScreen || video.msRequestFullscreen ) { this.isFullScreen() ? this.goOutFullscreen(video) : this.goInFullscreen(video); } else { elemDisplay('__videoFS', false); // Maximize video with CSS video.style.position = 'fixed'; video.style.top = 0; video.style.left = 0; video.style.width = '100vw'; video.style.height = '100vh'; video.style.zIndex = 9999; // Add a close/maximize button for fallback let isMaximized = true; const closeBtn = document.createElement('button'); closeBtn.innerText = isMaximized ? 'Minimize' : 'Maximize'; closeBtn.style.position = 'absolute'; closeBtn.style.top = '1px'; closeBtn.style.left = '1px'; closeBtn.style.zIndex = 10000; closeBtn.style.background = 'rgba(0,0,0,0.5)'; closeBtn.style.color = '#fff'; closeBtn.style.border = 'none'; closeBtn.style.padding = '8px 12px'; closeBtn.style.borderRadius = '4px'; closeBtn.style.cursor = 'pointer'; closeBtn.onclick = () => { if (isMaximized) { video.style.position = ''; video.style.top = ''; video.style.left = ''; video.style.width = ''; video.style.height = ''; video.style.zIndex = ''; closeBtn.innerText = 'Maximize'; isMaximized = false; } else { video.style.position = 'fixed'; video.style.top = 0; video.style.left = 0; video.style.width = '100vw'; video.style.height = '100vh'; video.style.zIndex = 9999; closeBtn.innerText = 'Minimize'; isMaximized = true; } }; // Ensure only one button is added if (!video.parentNode.querySelector('.mobile-video-close-btn')) { closeBtn.classList.add('mobile-video-close-btn'); video.parentNode.appendChild(closeBtn); } } }); const exitVideoBtn = this.getId(e.id); exitVideoBtn.addEventListener('click', (e) => { e.preventDefault(); if (this._moderator.media_cant_sharing) { return userLog('warning', 'The moderator does not allow you close this media', 'top-end', 6000); } this.closeVideo(true); }); this.handlePN(video.id, pn.id, d.id); if (!this.isMobileDevice) { this.setTippy(pn.id, 'Toggle Pin video player', 'bottom'); this.setTippy(e.id, 'Close video player', 'bottom'); this.setTippy(fsBtn.id, 'Full screen', 'bottom'); } handleAspectRatio(); console.log('[openVideo] Video-element-count', this.videoMediaContainer.childElementCount); this.sound('joined'); } closeVideo(emit = false, peer_id = 'all') { if (emit) { let data = { peer_id: peer_id, peer_name: this.peer_name, action: 'close', }; this.socket.emit('shareVideoAction', data); } let shareVideoDiv = this.getId('__shareVideo'); if (shareVideoDiv) { hide(videoCloseBtn); shareVideoDiv.parentNode.removeChild(shareVideoDiv); //alert(this.isVideoPinned + ' - ' + this.pinnedVideoPlayerId); if (this.isVideoPinned && this.pinnedVideoPlayerId == '__videoShare') { this.removeVideoPinMediaContainer(); console.log('Remove pin container due the Video player close'); } handleAspectRatio(); console.log('[closeVideo] Video-element-count', this.videoMediaContainer.childElementCount); this.sound('left'); } } // #################################################### // ROOM ACTION // #################################################### roomAction(action, emit = true, popup = true) { const data = { room_broadcasting: isBroadcastingEnabled, room_id: this.room_id, peer_id: this.peer_id, peer_name: this.peer_name, peer_uuid: this.peer_uuid, action: action, password: null, }; if (emit) { switch (action) { case 'broadcasting': this.socket.emit('roomAction', data); if (popup) this.roomStatus(action); break; case 'lock': if (room_password) { this.socket .request('getPeerCounts') .then(async (res) => { // Only the presenter can lock the room if (isPresenter || res.peerCounts == 1) { isPresenter = true; this.peer_info.peer_presenter = isPresenter; this.getId('isUserPresenter').innerText = isPresenter; data.password = room_password; this.socket.emit('roomAction', data); if (popup) this.roomStatus(action); } }) .catch((err) => { console.log('Get peer counts:', err); }); } else { Swal.fire({ allowOutsideClick: false, allowEscapeKey: false, showDenyButton: true, background: swalBackground, imageUrl: image.locked, input: 'text', inputPlaceholder: 'Set Room password', confirmButtonText: `OK`, denyButtonText: `Cancel`, showClass: { popup: 'animate__animated animate__fadeInDown' }, hideClass: { popup: 'animate__animated animate__fadeOutUp' }, inputValidator: (pwd) => { if (!pwd) return 'Please enter the Room password'; this.RoomPassword = pwd; }, }).then((result) => { if (result.isConfirmed) { data.password = this.RoomPassword; this.socket.emit('roomAction', data); this.roomStatus(action); } }); } break; case 'unlock': this.socket.emit('roomAction', data); if (popup) this.roomStatus(action); break; case 'lobbyOn': this.socket.emit('roomAction', data); if (popup) this.roomStatus(action); break; case 'lobbyOff': this.socket.emit('roomAction', data); if (popup) this.roomStatus(action); break; case 'hostOnlyRecordingOn': this.socket.emit('roomAction', data); if (popup) this.roomStatus(action); break; case 'hostOnlyRecordingOff': this.socket.emit('roomAction', data); if (popup) this.roomStatus(action); break; case 'isBanned': this.socket.emit('roomAction', data); this.isBanned(); break; default: break; } } else { this.roomStatus(action); } } roomStatus(action) { switch (action) { case 'broadcasting': this.userLog('info', `${icons.room} BROADCASTING ${isBroadcastingEnabled ? 'On' : 'Off'}`, 'top-end'); break; case 'lock': if (!isPresenter) return; this.sound('locked'); this.event(_EVENTS.roomLock); this.userLog('info', `${icons.lock} LOCKED the room by the password`, 'top-end'); break; case 'unlock': if (!isPresenter) return; this.userLog('info', `${icons.unlock} UNLOCKED the room`, 'top-end'); this.event(_EVENTS.roomUnlock); break; case 'lobbyOn': this.event(_EVENTS.lobbyOn); this.userLog('info', `${icons.lobby} Lobby is enabled`, 'top-end'); break; case 'lobbyOff': this.event(_EVENTS.lobbyOff); this.userLog('info', `${icons.lobby} Lobby is disabled`, 'top-end'); break; case 'hostOnlyRecordingOn': this.event(_EVENTS.hostOnlyRecordingOn); this.userLog('info', `${icons.recording} Host only recording is enabled`, 'top-end'); break; case 'hostOnlyRecordingOff': this.event(_EVENTS.hostOnlyRecordingOff); this.userLog('info', `${icons.recording} Host only recording is disabled`, 'top-end'); break; default: break; } } roomMessage(action, active = false) { const status = active ? 'ON' : 'OFF'; this.sound('switch'); switch (action) { case 'toggleVideoMirror': this.userLog('info', `${icons.mirror} Video mirror ${status}`, 'top-end'); break; case 'pitchBar': this.userLog('info', `${icons.pitchBar} Audio pitch bar ${status}`, 'top-end'); break; case 'sounds': this.userLog('info', `${icons.sounds} Sounds notification ${status}`, 'top-end'); break; case 'ptt': this.userLog('info', `${icons.ptt} Push to talk ${status}`, 'top-end'); break; case 'notify': this.userLog('info', `${icons.share} Share room on join ${status}`, 'top-end'); break; case 'hostOnlyRecording': this.userLog('info', `${icons.recording} Only host recording ${status}`, 'top-end'); break; case 'showChat': active ? this.userLog('info', `${icons.chat} Chat will be shown, when you receive a message`, 'top-end') : this.userLog( 'info', `${icons.chat} Chat not will be shown, when you receive a message`, 'top-end' ); break; case 'speechMessages': this.userLog('info', `${icons.speech} Speech incoming messages ${status}`, 'top-end'); break; case 'transcriptShowOnMsg': active ? this.userLog( 'info', `${icons.transcript} Transcript will be shown, when you receive a message`, 'top-end' ) : this.userLog( 'info', `${icons.transcript} Transcript not will be shown, when you receive a message`, 'top-end' ); break; case 'video_start_privacy': this.userLog( 'info', `${icons.moderator} Moderator: everyone starts in privacy mode ${status}`, 'top-end' ); break; case 'audio_start_muted': this.userLog('info', `${icons.moderator} Moderator: everyone starts muted ${status}`, 'top-end'); break; case 'video_start_hidden': this.userLog('info', `${icons.moderator} Moderator: everyone starts hidden ${status}`, 'top-end'); break; case 'audio_cant_unmute': this.userLog( 'info', `${icons.moderator} Moderator: everyone can't unmute themselves ${status}`, 'top-end' ); break; case 'video_cant_unhide': this.userLog( 'info', `${icons.moderator} Moderator: everyone can't unhide themselves ${status}`, 'top-end' ); break; case 'screen_cant_share': this.userLog( 'info', `${icons.moderator} Moderator: everyone can't share the screen ${status}`, 'top-end' ); break; case 'chat_cant_privately': this.userLog( 'info', `${icons.moderator} Moderator: everyone can't chat privately ${status}`, 'top-end' ); break; case 'chat_cant_chatgpt': this.userLog( 'info', `${icons.moderator} Moderator: everyone can't chat with ChatGPT ${status}`, 'top-end' ); break; case 'chat_cant_deep_seek': this.userLog( 'info', `${icons.moderator} Moderator: everyone can't chat with DeepSeek ${status}`, 'top-end' ); break; case 'media_cant_sharing': this.userLog('info', `${icons.moderator} Moderator: everyone can't share media ${status}`, 'top-end'); break; case 'disconnect_all_on_leave': this.userLog('info', `${icons.moderator} Moderator: disconnect all on leave room ${status}`, 'top-end'); break; case 'recSyncServer': this.userLog('info', `${icons.recSync} Server Sync Recording ${status}`, 'top-end'); break; case 'customThemeKeep': this.userLog('info', `${icons.theme} Custom theme keep ${status}`, 'top-end'); break; case 'save_room_notifications': this.userLog('success', 'Room notifications saved successfully', 'top-end'); break; default: break; } } async roomPassword(data) { switch (data.password) { case 'OK': this.RoomPasswordValid = true; await this.joinAllowed(data.room); break; case 'KO': this.RoomPasswordValid = false; this.roomIsLocked(); break; default: break; } } // #################################################### // ROOM LOBBY // #################################################### async roomLobby(data) { console.log('LOBBY--->', data); switch (data.lobby_status) { case 'waiting': if (!isRulesActive || isPresenter) { const { peer_id, peer_name, peer_avatar } = data; this.lobbyAddPear({ peer_id, peer_name, peer_avatar }); this.userLog('info', peer_name + ' wants to join the meeting', 'top-end'); } break; case 'accept': if (this.lobbyRemovePearForPresenter(data)) { return; } this.RoomLobbyAccepted = true; await this.joinAllowed(data.room); control.style.display = 'flex'; bottomButtons.style.display = 'flex'; this.msgPopup('info', 'Your join meeting request was accepted by the moderator', 3000, 'top'); break; case 'reject': if (this.lobbyRemovePearForPresenter(data)) { return; } this.RoomLobbyAccepted = false; this.sound('eject'); Swal.fire({ icon: 'warning', allowOutsideClick: false, allowEscapeKey: true, showDenyButton: false, showConfirmButton: true, background: swalBackground, title: 'Rejected', text: 'Your join meeting request was rejected by the moderator', confirmButtonText: `Ok`, showClass: { popup: 'animate__animated animate__fadeInDown' }, hideClass: { popup: 'animate__animated animate__fadeOutUp' }, }).then((result) => { if (result.isConfirmed) { this.exit(); } }); break; default: break; } } lobbyRemovePearForPresenter(data) { const peers_id = data.peers_id?.length > 0 ? data.peers_id : [data.peer_id]; // This current pear is in lobby accept request // It means that most probably we this pear is eaitin in lobby right now // so no need to update lobby list UI modal since there is no one if (peers_id.includes(this.peer_id)) { return false; } for (const peer_id of peers_id) { this.lobbyRemovePear(peer_id); } return true; } lobbyAction(id, lobby_status) { const words = id.split('___'); const peer_name = words[0]; const peer_id = words[1]; const data = { room_id: this.room_id, peer_id: peer_id, peer_name: peer_name, lobby_status: lobby_status, broadcast: true, }; this.socket.emit('roomLobby', data); this.lobbyRemovePear(peer_id); } lobbyAcceptAll() { const lobbyPearsIds = this.lobbyGetPeerIds(); console.log('lobbyAcceptAll', lobbyPearsIds, lobbyPearsIds.length); if (lobbyPearsIds.length > 0) { const data = this.lobbyGetData('accept', lobbyPearsIds); this.socket.emit('roomLobby', data); this.lobbyRemoveAll(); } else { this.userLog('info', 'No participants in lobby detected', 'top-end'); } } lobbyRejectAll() { const lobbyPearsIds = this.lobbyGetPeerIds(); if (lobbyPearsIds.length > 0) { const data = this.lobbyGetData('reject', lobbyPearsIds); this.socket.emit('roomLobby', data); this.lobbyRemoveAll(); } else { this.userLog('info', 'No participants in lobby detected', 'top-end'); } } lobbyRemoveAll() { this.lobbyPears = {}; this.lobbyRefreshUi(); } lobbyRemoveMe(peer_id) { this.lobbyRemovePear(peer_id); } lobbyAddPear(data) { this.lobbyPears[data.peer_id] = data; this.lobbyRefreshUi(); } lobbyRemovePear(peer_id) { delete this.lobbyPears[peer_id]; this.lobbyRefreshUi(); } lobbyRefreshUi() { let lobbyTr = this.getId('lobbyTbTemplate').innerHTML; const lobbyTb = this.getId('lobbyTb'); for (const peer_id of Object.keys(this.lobbyPears)) { const { peer_name, peer_avatar } = this.lobbyPears[peer_id]; const avatarImg = peer_avatar && this.isImageURL(peer_avatar) ? peer_avatar : this.isValidEmail(peer_name) ? this.genGravatar(peer_name, 32) : this.genAvatarSvg(peer_name, 32); const lobbyAcceptId = `${peer_name}___${peer_id}___lobbyAccept`; const lobbyRejectId = `${peer_name}___${peer_id}___lobbyReject`; lobbyTr += `
${rtmp}
`, showDenyButton: false, showCancelButton: false, confirmButtonText: `Copy URL`, showClass: { popup: 'animate__animated animate__fadeInDown' }, hideClass: { popup: 'animate__animated animate__fadeOutUp' }, }).then((result) => { if (result.isConfirmed) { copyToClipboard(rtmp); } }); } // #################################################### // ROOM SNAPSHOT WINDOW/SCREEN/TAB // #################################################### async snapshotRoom() { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const video = document.createElement('video'); try { const captureStream = await navigator.mediaDevices.getDisplayMedia({ video: true, }); video.srcObject = captureStream; video.onloadedmetadata = () => { video.play(); }; // Wait for the video to start playing video.onplay = async () => { this.sound('snapshot'); // Sleep some ms await this.sleep(1000); canvas.width = video.videoWidth; canvas.height = video.videoHeight; context.drawImage(video, 0, 0, canvas.width, canvas.height); // Create a link element to download the image const link = document.createElement('a'); link.href = canvas.toDataURL('image/png'); link.download = 'Room_' + this.room_id + '_' + getDataTimeString() + '_snapshot.png'; link.click(); // Stop all video tracks to release the capture stream captureStream.getTracks().forEach((track) => track.stop()); // Clean up: remove references to avoid memory leaks video.srcObject = null; canvas.width = 0; canvas.height = 0; }; } catch (err) { console.error('Error: ' + err); this.userLog('error', 'Snapshot room error ' + err.message, 'top-end', 6000); } } // #################################################### // ROOM NOTIFICATIONS // #################################################### cleanNotifications() { getId('notifyEmailInput').value = ''; getId('switchNotifyUserJoin').checked = false; return true; } saveNotifications(validate = true) { if (validate && !this.isValidNotifications()) return; const data = this.getNotificationsData(); if (!data) return; this.setNotificationsData(data); } setNotificationsData(data) { this.socket.emit('updateRoomNotifications', data, (response) => { response.error ? this.cleanNotifications() && this.userLog('warning', response.error, 'top-end', 6000) : this.roomMessage('save_room_notifications', true); }); } isValidNotifications() { const notifyEmailInput = getId('notifyEmailInput'); if (!this.isValidEmail(notifyEmailInput.value)) { notifyEmailInput.value = ''; this.userLog('warning', 'Email not valid', 'top-end', 6000); return false; } return true; } getNotificationsData() { const notifyEmailInput = getId('notifyEmailInput'); const switchNotifyUserJoin = getId('switchNotifyUserJoin'); return { peer_name: this.peer_name, peer_uuid: this.peer_uuid, notifications: { mode: { email: notifyEmailInput.value, //slack... }, events: { join: switchNotifyUserJoin.checked, // leave... }, }, }; } // #################################################### // HELPERS // #################################################### toggleVideoMirror() { const peerVideo = this.getName(this.peer_id); if (peerVideo) peerVideo.classList.toggle('mirror'); } sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } } // End