2024-04-21 14:42:52 +02:00

6696 lines
257 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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.4.20
*
*/
const cfg = {
useAvatarSvg: true,
};
const html = {
newline: '\n', //'<br />',
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 pulsate',
pip: 'fas fa-images',
fullScreen: 'fas fa-expand',
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-ellipsis-vertical',
};
const icons = {
room: '<i class="fas fa-home"></i>',
chat: '<i class="fas fa-comments"></i>',
user: '<i class="fas fa-user"></i>',
transcript: '<i class="fas fa-closed-captioning"></i>',
speech: '<i class="fas fa-volume-high"></i>',
share: '<i class="fas fa-share-alt"></i>',
ptt: '<i class="fa-solid fa-hand-pointer"></i>',
lobby: '<i class="fas fa-shield-halved"></i>',
lock: '<i class="fa-solid fa-lock"></i>',
unlock: '<i class="fa-solid fa-lock-open"></i>',
pitchBar: '<i class="fas fa-microphone-lines"></i>',
sounds: '<i class="fas fa-music"></i>',
fileSend: '<i class="fa-solid fa-file-export"></i>',
fileReceive: '<i class="fa-solid fa-file-import"></i>',
recording: '<i class="fas fa-record-vinyl"></i>',
moderator: '<i class="fas fa-m"></i>',
broadcaster: '<i class="fa-solid fa-wifi"></i>',
codecs: '<i class="fa-solid fa-film"></i>',
theme: '<i class="fas fa-fill-drip"></i>',
recSync: '<i class="fa-solid fa-cloud-arrow-up"></i>',
refresh: '<i class="fas fa-rotate"></i>',
};
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',
all: '../images/all.png',
forbidden: '../images/forbidden.png',
broadcasting: '../images/broadcasting.png',
geolocation: '../images/geolocation.png',
network: '../images/network.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',
};
// Enums
const enums = {
recording: {
started: 'Started conference recording',
start: 'Start conference recording',
stop: 'Stop conference recording',
},
//...
};
// 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.localAudioEl = localAudioEl;
this.remoteAudioEl = remoteAudioEl;
this.videoMediaContainer = videoMediaContainer;
this.videoPinMediaContainer = videoPinMediaContainer;
this.mediasoupClient = mediasoupClient;
this.socket = socket;
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;
// Moderator
this._moderator = {
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 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.isAudioAllowed = isAudioAllowed;
this.isVideoAllowed = isVideoAllowed;
this.isScreenAllowed = isScreenAllowed;
this.joinRoomWithScreen = joinRoomWithScreen;
this.producerTransport = null;
this.consumerTransport = null;
this.device = null;
this.isMobileDevice = DetectRTC.isMobileDevice;
this.isScreenShareSupported =
navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia ? true : false;
this.isMySettingsOpen = false;
this._isConnected = false;
this.isVideoOnFullScreen = false;
this.isVideoFullScreenSupported = peer_info.is_mobile_device && peer_info.os_name === 'iOS' ? false : true;
this.isVideoPictureInPictureSupported = document.pictureInPictureEnabled;
this.isZoomCenterMode = false;
this.isChatOpen = false;
this.isChatEmojiOpen = 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.camera = 'user';
this.videoQualitySelectedIndex = 0;
this.chatGPTContext = [];
this.chatMessages = [];
this.leftMsgAvatar = null;
this.rightMsgAvatar = null;
this.localVideoStream = null;
this.localAudioStream = null;
this.localScreenStream = null;
this.RoomPassword = false;
this.transcription = transcription;
// 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.mediaRecorder = null;
this.audioRecorder = null;
this.recScreenStream = null;
this.recSyncServerRecording = false;
this.recSyncTime = 4000; // 4 sec
this.recSyncChunkSize = 1000000; // 1MB
// Encodings
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.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
this.videoProducerId = null;
this.screenProducerId = null;
this.audioProducerId = null;
this.audioConsumers = 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 === 'unauthorized') {
console.log(
'00-WARNING ----> Room is Unauthorized for current user, please provide a valid username and password',
);
return this.userUnauthorized();
}
if (room === 'isLocked') {
this.event(_EVENTS.roomLock);
console.log('00-WARNING ----> Room is Locked, Try to unlock by the password');
return this.unlockTheRoom();
}
if (room === 'isLobby') {
this.event(_EVENTS.lobbyOn);
console.log('00-WARNING ----> Room Lobby Enabled, Wait to confirm my join');
return this.waitJoinConfirm();
}
if (room === 'isBanned') {
console.log('00-WARNING ----> You are Banned from the Room!');
return this.isBanned();
}
const peers = new Map(JSON.parse(room.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('00-WARNING ----> Username already in use');
return this.userNameAlreadyInRoom();
}
}
}
await this.joinAllowed(room);
})
.catch((error) => {
console.error('Join error:', error);
});
}
async joinAllowed(room) {
console.log('07 ----> Join Room allowed');
this.handleRoomInfo(room);
const data = await this.socket.request('getRouterRtpCapabilities');
this.device = await this.loadDevice(data);
console.log('07.3 ----> Get Router Rtp Capabilities codecs: ', this.device.rtpCapabilities.codecs);
await this.initTransports(this.device);
if (isBroadcastingEnabled) {
isPresenter ? this.startLocalMedia() : this.handleRoomBroadcasting();
} else {
this.startLocalMedia();
}
this.socket.emit('getProducers');
}
handleRoomInfo(room) {
console.log('07.0 ----> Room Survey', room.survey);
survey = room.survey;
console.log('07.0 ----> Room Leave Redirect', room.redirect);
redirect = room.redirect;
let peers = new Map(JSON.parse(room.peers));
participantsCount = peers.size;
// ME
for (let peer of Array.from(peers.keys()).filter((id) => id == this.peer_id)) {
let my_peer_info = 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;
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.recSyncServerRecording) {
console.log('07.1 WARNING ----> SERVER SYNC RECORDING ENABLED!');
this.recSyncServerRecording = localStorageSettings.rec_server;
if (BUTTONS.settings.tabRecording && !room.config.hostOnlyRecording) {
show(roomRecordingServer);
}
switchServerRecording.checked = this.recSyncServerRecording;
}
console.log('07.1 ----> SERVER SYNC RECORDING', this.recSyncServerRecording);
// ###################################################################################################
// Handle Room moderator rules
if (room.moderator && (!isRulesActive || !isPresenter)) {
console.log('07.2 ----> MODERATOR', room.moderator);
this._moderator.audio_start_muted = room.moderator.audio_start_muted;
this._moderator.video_start_hidden = room.moderator.video_start_hidden;
this._moderator.audio_cant_unmute = room.moderator.audio_cant_unmute;
this._moderator.video_cant_unhide = room.moderator.video_cant_unhide;
this._moderator.screen_cant_share = room.moderator.screen_cant_share;
this._moderator.chat_cant_privately = room.moderator.chat_cant_privately;
this._moderator.chat_cant_chatgpt = room.moderator.chat_cant_chatgpt;
//
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);
}
}
// PARTICIPANTS
for (let peer of Array.from(peers.keys()).filter((id) => id !== this.peer_id)) {
let peer_info = peers.get(peer).peer_info;
// console.log('07.1 ----> Remote Peer info', peer_info);
const canSetVideoOff = !isBroadcastingEnabled || (isBroadcastingEnabled && peer_info.peer_presenter);
if (!peer_info.peer_video && canSetVideoOff) {
console.log('Detected peer video off ' + peer_info.peer_name);
this.setVideoOff(peer_info, true);
}
if (peer_info.peer_recording) {
this.handleRecordingAction({
peer_id: peer_info.id,
peer_name: peer_info.peer_name,
action: enums.recording.started,
});
}
}
this.refreshParticipantsCount();
console.log('07.2 Participants Count ---->', participantsCount);
// notify && participantsCount == 1 ? shareRoom() : sound('joined');
if (notify && participantsCount == 1) {
shareRoom();
} else {
if (this.isScreenAllowed) {
this.shareScreen();
}
sound('joined');
}
}
async loadDevice(routerRtpCapabilities) {
let device;
try {
device = new this.mediasoupClient.Device();
} catch (error) {
if (error.name === 'UnsupportedError') {
console.error('Browser not supported');
this.userLog('error', 'Browser not supported', 'center', 6000);
} else {
console.error('Browser not supported: ', error);
this.userLog('error', 'Browser not supported: ' + error, 'center', 6000);
}
}
await device.load({
routerRtpCapabilities,
});
return device;
}
// ####################################################
// TRANSPORTS
// ####################################################
async initTransports(device) {
// ####################################################
// PRODUCER TRANSPORT
// ####################################################
const producerTransportData = await this.socket.request('createWebRtcTransport', {
forceTcp: false,
rtpCapabilities: device.rtpCapabilities,
});
if (producerTransportData.error) {
console.error(producerTransportData.error);
return;
}
this.producerTransport = device.createSendTransport(producerTransportData);
console.info('07.4 producerTransportData ---->', {
producerTransportId: this.producerTransport.id,
producerTransportData: producerTransportData,
});
this.producerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await this.socket.request('connectTransport', {
transport_id: this.producerTransport.id,
dtlsParameters,
});
callback();
} catch (err) {
errback(err);
}
});
this.producerTransport.on('produce', async ({ kind, appData, rtpParameters }, callback, errback) => {
console.log('Going to produce', { kind, appData, rtpParameters });
try {
const { producer_id } = await this.socket.request('produce', {
producerTransportId: this.producerTransport.id,
kind,
appData,
rtpParameters,
});
if (producer_id.error) {
errback(producer_id.error);
} else {
callback({ id: producer_id });
}
} catch (err) {
errback(err);
}
});
this.producerTransport.on('connectionstatechange', (state) => {
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.log('Producer Transport disconnected', { id: this.producerTransport.id });
/*
this.restartIce();
popupHtmlMessage(
null,
image.network,
'Producer Transport disconnected',
'Check Your Network Connectivity (Restarted ICE)',
'center',
);
*/
break;
case 'failed':
console.warn('Producer Transport failed', { id: this.producerTransport.id });
this.producerTransport.close();
popupHtmlMessage(
null,
image.network,
'Producer Transport failed',
'Check Your Network Connectivity',
'center',
);
break;
default:
console.log('Producer transport connection state changes', {
state: state,
id: this.producerTransport.id,
});
break;
}
});
this.producerTransport.on('icegatheringstatechange', (state) => {
console.log('Producer icegatheringstatechange', {
state: state,
id: this.producerTransport.id,
});
});
// ####################################################
// CONSUMER TRANSPORT
// ####################################################
const consumerTransportData = await this.socket.request('createWebRtcTransport', {
forceTcp: false,
});
if (consumerTransportData.error) {
console.error(consumerTransportData.error);
return;
}
this.consumerTransport = device.createRecvTransport(consumerTransportData);
console.info('07.5 consumerTransportData ---->', {
consumerTransportId: this.consumerTransport.id,
consumerTransportData: consumerTransportData,
});
this.consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await this.socket.request('connectTransport', {
transport_id: this.consumerTransport.id,
dtlsParameters,
});
callback();
} catch (err) {
errback(err);
}
});
this.consumerTransport.on('connectionstatechange', (state) => {
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.log('Consumer Transport disconnected', { id: this.consumerTransport.id });
/*
this.restartIce();
popupHtmlMessage(
null,
image.network,
'Consumer Transport disconnected',
'Check Your Network Connectivity (Restarted ICE)',
'center',
);
*/
case 'failed':
console.warn('Consumer Transport failed', { id: this.consumerTransport.id });
this.consumerTransport.close();
popupHtmlMessage(
null,
image.network,
'Consumer Transport failed',
'Check Your Network Connectivity',
'center',
);
break;
default:
console.log('Consumer transport connection state changes', {
state: state,
id: this.consumerTransport.id,
});
break;
}
});
this.consumerTransport.on('icegatheringstatechange', (state) => {
console.log('Consumer icegatheringstatechange', {
state: state,
id: this.consumerTransport.id,
});
});
// ####################################################
// TODO: DATA TRANSPORT
// ####################################################
//
}
// ####################################################
// RESTART ICE
// ####################################################
async restartIce() {
console.log('Restart ICE...');
try {
if (this.producerTransport) {
console.log('Restarting producer transport ICE');
const iceParameters = await this.socket.request('restartIce', {
transport_id: this.producerTransport.id,
});
await this.producerTransport.restartIce({ iceParameters });
}
if (this.consumerTransport) {
console.log('Restarting consumer transport ICE');
const iceParameters = await this.socket.request('restartIce', {
transport_id: this.consumerTransport.id,
});
await this.consumerTransport.restartIce({ iceParameters });
}
console.log('Restart ICE done');
} catch (error) {
console.error('Restart ICE error', error.message);
}
}
// ####################################################
// SOCKET ON
// ####################################################
initSockets() {
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('wbCanvasToJson', this.handleWbCanvasToJson);
this.socket.on('whiteboardAction', this.handleWhiteboardAction);
this.socket.on('audioVolume', this.handleAudioVolumeData);
this.socket.on('updateRoomModerator', this.handleUpdateRoomModeratorData);
this.socket.on('updateRoomModeratorALL', this.handleUpdateRoomModeratorALLData);
this.socket.on('recordingAction', this.handleRecordingActionData);
this.socket.on('connect', this.handleSocketConnect);
this.socket.on('disconnect', this.handleSocketDisconnect);
}
// ####################################################
// HANDLE SOCKET DATA
// ####################################################
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();
} else {
adaptAspectRatio(participantsCount);
}
};
handleNewProducers = async (data) => {
if (data.length > 0) {
console.log('SocketOn New producers', data);
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);
};
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);
};
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);
};
handleSocketConnect = () => {
console.log('SocketOn Connected to signaling server!');
this._isConnected = true;
this.refreshBrowser();
};
handleSocketDisconnect = () => {
this.exit(true);
this.ServerAway();
this.saveRecording('Socket disconnected');
};
// ####################################################
// SERVER AWAY/MAINTENANCE
// ####################################################
ServerAway() {
this.sound('alert');
window.localStorage.isReconnected = true;
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: true,
showConfirmButton: false,
background: swalBackground,
imageUrl: image.poster,
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);
}
});
}
refreshBrowser() {
getPeerName() ? location.reload() : openURL(this.getReconnectDirectJoinURL());
}
getReconnectDirectJoinURL() {
const baseUrl = `${window.location.origin}/join`;
const queryParams = {
room: this.room_id,
roomPassword: this.RoomPassword,
name: this.peer_name,
audio: this.peer_info.peer_audio,
video: this.peer_info.peer_video,
screen: this.peer_info.peer_screen,
notify: 0,
isPresenter: isPresenter,
};
if (this.peer_info.peer_token) queryParams.token = this.peer_info.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. <br/> 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) {
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 deactivate room broadcasting?',
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
switchBroadcasting.click();
}
});
}
// ####################################################
// START LOCAL AUDIO VIDEO MEDIA
// ####################################################
startLocalMedia() {
console.log('08 ----> Start local media');
if (this.isAudioAllowed && !this._moderator.audio_start_muted) {
console.log('09 ----> Start audio media');
this.produce(mediaType.audio, microphoneSelect.value);
} else {
console.log('09 ----> Audio is off');
setColor(startAudioButton, 'red');
this.setIsAudio(this.peer_id, false);
if (BUTTONS.main.startAudioButton) this.event(_EVENTS.stopAudio);
this.updatePeerInfo(this.peer_name, this.peer_id, 'audio', false);
}
if (this.isVideoAllowed && !this._moderator.video_start_hidden) {
console.log('10 ----> Start video media');
this.produce(mediaType.video, videoSelect.value);
} else {
console.log('10 ----> Video is off');
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);
}
if (this.joinRoomWithScreen && !this._moderator.screen_cant_share) {
console.log('08 ----> Start Screen media');
this.produce(mediaType.screen, null, false, true);
}
// if (this.isScreenAllowed) {
// this.shareScreen();
// }
}
// ####################################################
// PRODUCER
// ####################################################
async produce(type, deviceId = null, swapCamera = false, init = false) {
let mediaConstraints = {};
let audio = false;
let screen = false;
switch (type) {
case mediaType.audio:
this.isAudioAllowed = true;
mediaConstraints = this.getAudioConstraints(deviceId);
audio = true;
break;
case mediaType.video:
this.isVideoAllowed = true;
if (swapCamera) {
mediaConstraints = this.getCameraConstraints();
} else {
mediaConstraints = this.getVideoConstraints(deviceId);
}
break;
case mediaType.screen:
mediaConstraints = this.getScreenConstraints();
screen = true;
break;
default:
return;
}
if (!this.device.canProduce('video') && !audio) {
return console.error('Cannot produce video');
}
if (this.producerLabel.has(type)) {
return console.log('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);
let stream;
try {
if (init) {
stream = initStream;
} else {
stream = screen
? await navigator.mediaDevices.getDisplayMedia(mediaConstraints)
: await navigator.mediaDevices.getUserMedia(mediaConstraints);
}
console.log('Supported Constraints', navigator.mediaDevices.getSupportedConstraints());
const track = audio ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0];
console.log(`${type} settings ->`, track.getSettings());
const params = {
track,
appData: {
mediaType: type,
},
};
if (audio) {
console.log('AUDIO ENABLE OPUS');
params.codecOptions = {
opusStereo: true,
opusDtx: true,
opusFec: true,
opusNack: true,
};
}
if (!audio && !screen) {
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 (!audio && 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 PARAMS', params);
const producer = await this.producerTransport.produce(params);
if (!producer) {
throw new Error('Producer not found!');
}
console.log('PRODUCER', producer);
this.producers.set(producer.id, producer);
// if screen sharing produce the tab audio + microphone
if (screen && stream.getAudioTracks()[0]) {
this.produceScreenAudio(stream);
}
let elem, au;
if (!audio) {
this.localVideoStream = stream;
if (type == mediaType.video) this.videoProducerId = producer.id;
if (type == mediaType.screen) this.screenProducerId = producer.id;
elem = await this.handleProducer(producer.id, type, stream);
//if (!screen && !isEnumerateDevices) enumerateVideoDevices(stream);
} else {
this.localAudioStream = stream;
this.audioProducerId = producer.id;
au = await this.handleProducer(producer.id, type, stream);
//if (!isEnumerateDevices) enumerateAudioDevices(stream);
getMicrophoneVolumeIndicator(stream);
}
if (type == mediaType.video) {
this.handleHideMe();
}
producer.on('trackended', () => {
console.log('Producer track ended', { id: producer.id });
this.closeProducer(type);
});
producer.on('transportclose', () => {
console.log('Producer transport close', { id: producer.id });
if (!audio) {
const d = this.getId(producer.id + '__video');
elem.srcObject.getTracks().forEach(function (track) {
track.stop();
});
elem.parentNode.removeChild(elem);
d.parentNode.removeChild(d);
handleAspectRatio();
console.log('[transportClose] Video-element-count', this.videoMediaContainer.childElementCount);
} else {
au.srcObject.getTracks().forEach(function (track) {
track.stop();
});
au.parentNode.removeChild(au);
console.log('[transportClose] audio-element-count', this.localAudioEl.childElementCount);
}
this.producers.delete(producer.id);
});
producer.on('close', () => {
console.log('Closing producer', { id: producer.id });
if (!audio) {
const d = this.getId(producer.id + '__video');
elem.srcObject.getTracks().forEach(function (track) {
track.stop();
});
elem.parentNode.removeChild(elem);
d.parentNode.removeChild(d);
handleAspectRatio();
console.log('[closingProducer] Video-element-count', this.videoMediaContainer.childElementCount);
} else {
au.srcObject.getTracks().forEach(function (track) {
track.stop();
});
au.parentNode.removeChild(au);
console.log('[closingProducer] audio-element-count', this.localAudioEl.childElementCount);
}
this.producers.delete(producer.id);
});
this.producerLabel.set(type, producer.id);
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');
} catch (err) {
console.error('Produce error:', err);
handleMediaError(type, err);
if (!audio && !screen && videoQuality.selectedIndex != 0) {
videoQuality.selectedIndex = this.videoQualitySelectedIndex;
this.sound('alert');
this.userLog(
'error',
`Your device doesn't support the selected video quality (${videoQuality.value}), please select the another one.`,
'top-end',
);
}
}
}
// ####################################################
// AUDIO/VIDEO CONSTRAINTS
// ####################################################
getAudioConstraints(deviceId) {
let constraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
deviceId: deviceId,
},
video: false,
};
if (isRulesActive && isPresenter) {
constraints = {
audio: {
autoGainControl: switchAutoGainControl.checked,
echoCancellation: switchNoiseSuppression.checked,
noiseSuppression: switchEchoCancellation.checked,
sampleRate: parseInt(sampleRateSelect.value),
sampleSize: parseInt(sampleSizeSelect.value),
channelCount: parseInt(channelCountSelect.value),
latency: parseInt(micLatencyRange.value),
volume: parseInt(micVolumeRange.value / 100),
deviceId: deviceId,
},
video: false,
};
}
return constraints;
}
getCameraConstraints() {
this.camera = this.camera == 'user' ? 'environment' : 'user';
if (this.camera != 'user') this.camVideo = { facingMode: { exact: this.camera } };
else this.camVideo = true;
return {
audio: false,
video: this.camVideo,
};
}
getVideoConstraints(deviceId) {
const defaultFrameRate = {
min: 5,
ideal: 15,
max: 30,
};
const selectedValue = this.getSelectedIndexValue(videoFps);
const customFrameRate = parseInt(selectedValue, 10);
const frameRate = selectedValue == 'max' ? defaultFrameRate : customFrameRate;
let videoConstraints = {
audio: false,
video: {
width: {
min: 640,
ideal: 1920,
max: 3840,
},
height: {
min: 480,
ideal: 1080,
max: 2160,
},
deviceId: deviceId,
aspectRatio: 1.777, // 16:9
frameRate: frameRate,
},
}; // Init auto detect max cam resolution and fps
videoFps.disabled = false;
switch (videoQuality.value) {
case 'default':
// This will make the browser use HD Video and 30fps as default.
videoConstraints = {
audio: false,
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
deviceId: deviceId,
aspectRatio: 1.777,
},
};
videoFps.selectedIndex = 0;
videoFps.disabled = true;
break;
case 'qvga':
videoConstraints = {
audio: false,
video: {
width: { exact: 320 },
height: { exact: 240 },
deviceId: deviceId,
aspectRatio: 1.777,
frameRate: frameRate,
},
}; // video cam constraints low bandwidth
break;
case 'vga':
videoConstraints = {
audio: false,
video: {
width: { exact: 640 },
height: { exact: 480 },
deviceId: deviceId,
aspectRatio: 1.777,
frameRate: frameRate,
},
}; // video cam constraints medium bandwidth
break;
case 'hd':
videoConstraints = {
audio: false,
video: {
width: { exact: 1280 },
height: { exact: 720 },
deviceId: deviceId,
aspectRatio: 1.777,
frameRate: frameRate,
},
}; // video cam constraints high bandwidth
break;
case 'fhd':
videoConstraints = {
audio: false,
video: {
width: { exact: 1920 },
height: { exact: 1080 },
deviceId: deviceId,
aspectRatio: 1.777,
frameRate: frameRate,
},
}; // video cam constraints very high bandwidth
break;
case '2k':
videoConstraints = {
audio: false,
video: {
width: { exact: 2560 },
height: { exact: 1440 },
deviceId: deviceId,
aspectRatio: 1.777,
frameRate: frameRate,
},
}; // video cam constraints ultra high bandwidth
break;
case '4k':
videoConstraints = {
audio: false,
video: {
width: { exact: 3840 },
height: { exact: 2160 },
deviceId: deviceId,
aspectRatio: 1.777,
frameRate: frameRate,
},
}; // video cam constraints ultra high bandwidth
break;
default:
break;
}
this.videoQualitySelectedIndex = videoQuality.selectedIndex;
return videoConstraints;
}
getScreenConstraints() {
const selectedValue = this.getSelectedIndexValue(screenFps);
const frameRate = selectedValue == 'max' ? 30 : parseInt(selectedValue, 10);
return {
audio: true,
video: {
width: { max: 1920 },
height: { max: 1080 },
frameRate: frameRate,
},
};
}
// ####################################################
// WEBCAM ENCODING
// ####################################################
getWebCamEncoding() {
let encodings;
let codec;
console.log('WEBCAM ENCODING', {
forceVP8: this.forceVP8,
forceVP9: this.forceVP9,
forceH264: this.forceH264,
numSimulcastStreamsWebcam: this.numSimulcastStreamsWebcam,
enableWebcamLayers: this.enableWebcamLayers,
webcamScalabilityMode: this.webcamScalabilityMode,
});
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');
}
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 && codec) || firstVideoCodec.mimeType.toLowerCase() === 'video/vp9') {
console.log('WEBCAM ENCODING: VP9 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,
numSimulcastStreamsSharing: this.numSimulcastStreamsSharing,
enableSharingLayers: this.enableSharingLayers,
sharingScalabilityMode: this.sharingScalabilityMode,
});
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');
}
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 && codec) || firstVideoCodec.mimeType.toLowerCase() === 'video/vp9') {
console.log('SCREEN ENCODING: VP9 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 };
}
// ####################################################
// 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);
setTimeout(function () {
rc.produce(type, deviceId, swapCamera);
}, 1000);
}
async handleProducer(id, type, stream) {
let elem, vb, vp, ts, d, p, i, au, pip, fs, pm, pb, pn;
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);
!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)';
elem.className = this.isMobileDevice || isScreen ? '' : 'mirror';
vb = document.createElement('div');
vb.setAttribute('id', this.peer_id + '__vb');
vb.className = 'videoMenuBar fadein';
pip = document.createElement('button');
pip.id = id + '__pictureInPicture';
pip.className = html.pip;
fs = document.createElement('button');
fs.id = id + '__fullScreen';
fs.className = html.fullScreen;
ts = document.createElement('button');
ts.id = id + '__snapshot';
ts.className = html.snapshot;
pn = document.createElement('button');
pn.id = id + '__pin';
pn.className = html.pin;
vp = document.createElement('button');
vp.id = this.peer_id + +'__vp';
vp.className = html.videoPrivacy;
au = document.createElement('button');
au.id = this.peer_id + '__audio';
au.className = 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);
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.fullScreenButton && this.isVideoFullScreenSupported && vb.appendChild(fs);
if (!this.isMobileDevice) vb.appendChild(pn);
d.appendChild(elem);
d.appendChild(pm);
d.appendChild(i);
d.appendChild(p);
d.appendChild(vb);
this.videoMediaContainer.appendChild(d);
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.handleDD(elem.id, this.peer_id, true);
this.handleTS(elem.id, ts.id);
this.handlePN(elem.id, pn.id, d.id, isScreen);
this.handleZV(elem.id, d.id, this.peer_id);
if (!isScreen) this.handleVP(elem.id, vp.id);
this.popupPeerInfo(p.id, this.peer_info);
this.checkPeerInfoStatus(this.peer_info);
if (isScreen) pn.click();
handleAspectRatio();
if (!this.isMobileDevice) {
this.setTippy(pn.id, 'Toggle Pin', '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');
}
console.log('[addProducer] Video-element-count', this.videoMediaContainer.childElementCount);
break;
case mediaType.audio:
elem = document.createElement('audio');
elem.id = id + '__localAudio';
elem.controls = false;
elem.autoplay = true;
elem.muted = true;
elem.volume = 0;
this.myAudioEl = elem;
this.localAudioEl.appendChild(elem);
this.attachMediaStream(elem, stream, type, 'Producer');
console.log('[addProducer] audio-element-count', this.localAudioEl.childElementCount);
break;
default:
break;
}
return elem;
}
async pauseProducer(type) {
if (!this.producerLabel.has(type)) {
return console.log('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: producer_id });
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.log('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: producer_id });
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) {
if (!this.producerLabel.has(type)) {
return console.log('There is no producer for this type ' + type);
}
const producer_id = this.producerLabel.get(type);
const data = {
peer_name: this.peer_name,
producer_id: producer_id,
type: type,
status: false,
};
console.log('Close producer', data);
this.socket.emit('producerClosed', data);
this.producers.get(producer_id).close();
this.producers.delete(producer_id);
this.producerLabel.delete(type);
if (type !== mediaType.audio) {
const elem = this.getId(producer_id);
const d = this.getId(producer_id + '__video');
elem.srcObject.getTracks().forEach(function (track) {
track.stop();
});
d.parentNode.removeChild(d);
//alert(this.pinnedVideoPlayerId + '==' + producer_id);
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,
});
}
handleAspectRatio();
console.log('[producerClose] Video-element-count', this.videoMediaContainer.childElementCount);
}
if (type === mediaType.audio) {
const au = this.getId(producer_id + '__localAudio');
au.srcObject.getTracks().forEach(function (track) {
track.stop();
});
this.localAudioEl.removeChild(au);
console.log('[producerClose] Audio-element-count', this.localAudioEl.childElementCount);
}
switch (type) {
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.log('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);
const sa = await this.handleProducer(producerSa.id, mediaType.audio, stream);
producerSa.on('trackended', () => {
console.log('Producer Screen audio track ended', { id: producerSa.id });
this.closeProducer(mediaType.audioTab);
});
producerSa.on('transportclose', () => {
console.log('Producer Screen audio transport close', { id: producerSa.id });
sa.srcObject.getTracks().forEach(function (track) {
track.stop();
});
sa.parentNode.removeChild(sa);
console.log('[transportClose] audio-element-count', this.localAudioEl.childElementCount);
this.producers.delete(producerSa.id);
});
producerSa.on('close', () => {
console.log('Closing Screen audio producer', { id: producerSa.id });
sa.srcObject.getTracks().forEach(function (track) {
track.stop();
});
sa.parentNode.removeChild(sa);
console.log('[closingProducer] audio-element-count', this.localAudioEl.childElementCount);
this.producers.delete(producerSa.id);
});
this.producerLabel.set(mediaType.audioTab, producerSa.id);
} catch (err) {
console.error('Produce error:', err);
}
}
// ####################################################
// CONSUMER
// ####################################################
async consume(producer_id, peer_name, peer_info, type) {
try {
wbUpdate();
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);
consumer.on('trackended', () => {
console.log('Consumer track end', { id: consumer.id });
this.removeConsumer(consumer.id, consumer.kind);
});
consumer.on('transportclose', () => {
console.log('Consumer transport close', { id: consumer.id });
this.removeConsumer(consumer.id, consumer.kind);
});
this.handleConsumer(consumer.id, type, stream, peer_name, peer_info);
if (kind === 'video' && isParticipantsListOpen) {
await getRoomParticipants();
}
} catch (error) {
console.error('Error in consume', error);
}
}
async getConsumeStream(producerId, peer_id, type) {
const { rtpCapabilities } = this.device;
const data = await this.socket.request('consume', {
rtpCapabilities,
consumerTransportId: this.consumerTransport.id,
producerId,
});
console.log('DATA', data);
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,
};
}
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;
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 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);
!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.setAttribute('id', remotePeerId + '__vb');
vb.className = 'videoMenuBar fadein';
eDiv = document.createElement('div');
eDiv.className = 'expand-video';
eBtn = document.createElement('button');
eBtn.id = remotePeerId + '_videoExpandBtn';
eBtn.className = html.expand;
eVc = document.createElement('div');
eVc.className = 'expand-video-content';
pv = document.createElement('input');
pv.id = remotePeerId + '___pVolume';
pv.type = 'range';
pv.min = 0;
pv.max = 100;
pv.value = 100;
pip = document.createElement('button');
pip.id = id + '__pictureInPicture';
pip.className = html.pip;
fs = document.createElement('button');
fs.id = id + '__fullScreen';
fs.className = html.fullScreen;
ts = document.createElement('button');
ts.id = id + '__snapshot';
ts.className = html.snapshot;
pn = document.createElement('button');
pn.id = id + '__pin';
pn.className = html.pin;
sf = document.createElement('button');
sf.id = id + '___' + remotePeerId + '___sendFile';
sf.className = html.sendFile;
sm = document.createElement('button');
sm.id = id + '___' + remotePeerId + '___sendMsg';
sm.className = html.sendMsg;
sv = document.createElement('button');
sv.id = id + '___' + remotePeerId + '___sendVideo';
sv.className = html.sendVideo;
cm = document.createElement('button');
cm.id = id + '___' + remotePeerId + '___video';
cm.className = html.videoOn;
au = document.createElement('button');
au.id = remotePeerId + '__audio';
au.className = remotePeerAudio ? html.audioOn : html.audioOff;
gl = document.createElement('button');
gl.id = id + '___' + remotePeerId + '___geoLocation';
gl.className = html.geolocation;
ban = document.createElement('button');
ban.id = id + '___' + remotePeerId + '___ban';
ban.className = html.ban;
ko = document.createElement('button');
ko.id = id + '___' + remotePeerId + '___kickOut';
ko.className = 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);
BUTTONS.consumerVideo.sendMessageButton && eVc.appendChild(sm);
BUTTONS.consumerVideo.sendFileButton && eVc.appendChild(sf);
BUTTONS.consumerVideo.sendVideoButton && eVc.appendChild(sv);
BUTTONS.consumerVideo.geolocationButton && eVc.appendChild(gl);
BUTTONS.consumerVideo.banButton && eVc.appendChild(ban);
BUTTONS.consumerVideo.ejectButton && eVc.appendChild(ko);
eDiv.appendChild(eBtn);
eDiv.appendChild(eVc);
vb.appendChild(eDiv);
BUTTONS.consumerVideo.audioVolumeInput && !this.isMobileDevice && vb.appendChild(pv);
vb.appendChild(au);
vb.appendChild(cm);
BUTTONS.consumerVideo.snapShotButton && vb.appendChild(ts);
BUTTONS.consumerVideo.videoPictureInPicture &&
this.isVideoPictureInPictureSupported &&
vb.appendChild(pip);
BUTTONS.consumerVideo.fullScreenButton && this.isVideoFullScreenSupported && vb.appendChild(fs);
if (!this.isMobileDevice) vb.appendChild(pn);
d.appendChild(elem);
d.appendChild(i);
d.appendChild(p);
d.appendChild(pm);
d.appendChild(vb);
this.videoMediaContainer.appendChild(d);
this.attachMediaStream(elem, stream, type, 'Consumer');
this.isVideoPictureInPictureSupported && this.handlePIP(elem.id, pip.id);
this.isVideoFullScreenSupported && this.handleFS(elem.id, fs.id);
this.handleDD(elem.id, remotePeerId);
this.handleTS(elem.id, ts.id);
this.handleSF(sf.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.handlePV(id + '___' + pv.id);
this.handleGL(gl.id);
this.handleBAN(ban.id);
this.handleKO(ko.id);
this.handlePN(elem.id, pn.id, d.id, remoteIsScreen);
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) pn.click();
this.sound('joined');
handleAspectRatio();
console.log('[addConsumer] Video-element-count', this.videoMediaContainer.childElementCount);
if (!this.isMobileDevice) {
this.setTippy(pn.id, 'Toggle Pin', 'bottom');
this.setTippy(pip.id, 'Toggle picture in picture', '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');
}
this.setPeerAudio(remotePeerId, remotePeerAudio);
break;
case mediaType.audio:
elem = document.createElement('audio');
elem.id = id;
elem.autoplay = true;
elem.audio = 1.0;
this.remoteAudioEl.appendChild(elem);
this.attachMediaStream(elem, stream, type, 'Consumer');
let audioConsumerId = remotePeerId + '___pVolume';
this.audioConsumers.set(audioConsumerId, id);
let inputPv = this.getId(audioConsumerId);
if (inputPv) {
this.handlePV(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) {
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');
if (d) {
d.parentNode.removeChild(d);
//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.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_info.peer_id;
const peer_name = peer_info.peer_name;
const peer_audio = peer_info.peer_audio;
const peer_presenter = peer_info.peer_presenter;
this.removeVideoOff(peer_id);
d = document.createElement('div');
d.className = 'Camera';
d.id = peer_id + '__videoOff';
vb = document.createElement('div');
vb.setAttribute('id', peer_id + 'vb');
vb.className = 'videoMenuBar fadein';
au = document.createElement('button');
au.id = peer_id + '__audio';
au.className = peer_audio ? html.audioOn : html.audioOff;
if (remotePeer) {
pv = document.createElement('input');
pv.id = peer_id + '___pVolume';
pv.type = 'range';
pv.min = 0;
pv.max = 100;
pv.value = 100;
sf = document.createElement('button');
sf.id = 'remotePeer___' + peer_id + '___sendFile';
sf.className = html.sendFile;
sm = document.createElement('button');
sm.id = 'remotePeer___' + peer_id + '___sendMsg';
sm.className = html.sendMsg;
sv = document.createElement('button');
sv.id = 'remotePeer___' + peer_id + '___sendVideo';
sv.className = html.sendVideo;
gl = document.createElement('button');
gl.id = 'remotePeer___' + peer_id + '___geoLocation';
gl.className = html.geolocation;
ban = document.createElement('button');
ban.id = 'remotePeer___' + peer_id + '___ban';
ban.className = html.ban;
ko = document.createElement('button');
ko.id = 'remotePeer___' + peer_id + '___kickOut';
ko.className = 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 && !this.isMobileDevice && vb.appendChild(pv);
}
vb.appendChild(au);
d.appendChild(i);
d.appendChild(p);
d.appendChild(h);
d.appendChild(pm);
d.appendChild(vb);
this.videoMediaContainer.appendChild(d);
BUTTONS.videoOff.muteAudioButton && this.handleAU(au.id);
if (remotePeer) {
this.handlePV('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);
}
this.handleDD(d.id, peer_id, !remotePeer);
this.popupPeerInfo(p.id, peer_info);
this.setVideoAvatarImgName(i.id, peer_name);
this.getId(i.id).style.display = 'block';
handleAspectRatio();
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);
console.log('[setVideoOff] Video-element-count', this.videoMediaContainer.childElementCount);
//
wbUpdate();
this.handleHideMe();
}
removeVideoOff(peer_id) {
let pvOff = this.getId(peer_id + '__videoOff');
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) {
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() {
//...
if (isPresenter && switchDisconnectAllOnLeave.checked) {
this.ejectAllOnLeave();
}
this.exit();
}
// ####################################################
// 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
// ####################################################
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);
}
async changeAudioDestination(audioElement = false) {
const audioDestination = speakerSelect.value;
if (audioElement) {
await this.attachSinkId(audioElement, audioDestination);
} else {
const audioElements = this.remoteAudioEl.querySelectorAll('audio');
audioElements.forEach(async (audioElement) => {
await this.attachSinkId(audioElement, audioDestination);
});
}
}
async attachSinkId(elem, sinkId) {
if (typeof elem.sinkId !== 'undefined') {
elem.setSinkId(sinkId)
.then(() => {
console.log(`Success, audio output device attached: ${sinkId}`);
})
.catch((err) => {
let errorMessage = err;
let speakerSelect = this.getId('speakerSelect');
if (err.name === 'SecurityError')
errorMessage = `You need to use HTTPS for selecting audio output device: ${err}`;
console.error('Attach SinkId error: ', errorMessage);
this.userLog('error', errorMessage, 'top-end', 6000);
speakerSelect.selectedIndex = 0;
refreshLsDevices();
});
} else {
const error = `Browser seems doesn't support output device selection.`;
console.warn(error);
this.userLog('error', error, 'top-end', 6000);
}
}
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 (DetectRTC.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) {
let elem = this.getId(elemId);
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 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
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 = `
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="${avatarImgSize}px"
height="${avatarImgSize}px"
viewBox="0 0 ${avatarImgSize} ${avatarImgSize}"
version="1.1">
<circle
fill="${bgColor}"
width="${avatarImgSize}"
height="${avatarImgSize}"
cx="${avatarImgSize / 2}"
cy="${avatarImgSize / 2}"
r="${avatarImgSize / 2}"
/>
<text
x="50%"
y="50%"
style="color:${textColor};
line-height:1;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Fira Sans, Droid Sans, Helvetica Neue, sans-serif"
alignment-baseline="middle"
text-anchor="middle"
font-size="${Math.round(avatarImgSize * 0.4)}"
font-weight="normal"
dy=".1em"
dominant-baseline="middle"
fill="${textColor}">${peerName.substring(0, 2).toUpperCase()}
</text>
</svg>`;
return 'data:image/svg+xml,' + svg.replace(/#/g, '%23').replace(/"/g, "'").replace(/&/g, '&amp;');
}
setPeerAudio(peer_id, status) {
console.log('Set peer audio enabled: ' + status);
const audioStatus = this.getPeerAudioBtn(peer_id); // producer, consumers
const audioVolume = this.getPeerAudioVolumeBtn(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 audio enabled: ' + status);
this.peer_info.peer_audio = status;
const audioStatus = this.getPeerAudioBtn(peer_id); // producer, consumers
if (audioStatus) audioStatus.className = status ? html.audioOn : html.audioOff;
}
}
setIsVideo(status) {
if (!isBroadcastingEnabled || (isBroadcastingEnabled && isPresenter)) {
this.peer_info.peer_video = status;
if (!this.peer_info.peer_video) {
console.log('Set 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 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);
}
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');
}
getPeerAudioVolumeBtn(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) {
if (!isSoundEnabled && !force) return;
let sound = '../sounds/' + name + '.wav';
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' },
});
}
}
msgPopup(type, message) {
switch (type) {
case 'warning':
case 'error':
Swal.fire({
background: swalBackground,
position: 'center',
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: 'center',
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: 'center',
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: 3000,
});
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 + '<br/> 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);
if (this.consumers.size > 0 || participantsCount > 1) {
return true;
}
return false;
}
// ####################################################
// 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 ? false : true;
}
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');
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', '-160px');
document.documentElement.style.setProperty('--btns-width', '320px');
document.documentElement.style.setProperty('--btns-flex-direction', 'row');
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);
});
}
});
}
}
// ####################################################
// FULL SCREEN
// ####################################################
toggleFullScreen(elem = null) {
let el = elem ? elem : document.documentElement;
document.fullscreenEnabled =
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled;
document.exitFullscreen =
document.exitFullscreen ||
document.webkitExitFullscreen ||
document.mozCancelFullScreen ||
document.msExitFullscreen;
el.requestFullscreen =
el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullScreen;
if (document.fullscreenEnabled) {
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
? document.exitFullscreen()
: el.requestFullscreen();
}
if (elem == null) this.isVideoOnFullScreen = document.fullscreenEnabled;
}
handleFS(elemId, fsId) {
let videoPlayer = this.getId(elemId);
let btnFs = this.getId(fsId);
if (btnFs) {
this.setTippy(fsId, 'Full screen', 'bottom');
btnFs.addEventListener('click', () => {
if (videoPlayer.classList.contains('videoCircle')) {
return userLog('info', 'Full Screen not allowed if video on privacy mode', 'top-end');
}
videoPlayer.style.pointerEvents = this.isVideoOnFullScreen ? 'auto' : 'none';
this.toggleFullScreen(videoPlayer);
this.isVideoOnFullScreen = this.isVideoOnFullScreen ? false : true;
});
}
if (videoPlayer) {
videoPlayer.addEventListener('click', () => {
if (videoPlayer.classList.contains('videoCircle')) {
return userLog('info', 'Full Screen not allowed if video on privacy mode', 'top-end');
}
if (!videoPlayer.hasAttribute('controls')) {
if ((this.isMobileDevice && this.isVideoOnFullScreen) || !this.isMobileDevice) {
videoPlayer.style.pointerEvents = this.isVideoOnFullScreen ? 'auto' : 'none';
this.toggleFullScreen(videoPlayer);
this.isVideoOnFullScreen = this.isVideoOnFullScreen ? false : true;
}
}
});
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) {
let videoPlayer = this.getId(elemId);
let btnPn = this.getId(pnId);
let cam = this.getId(camId);
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);
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);
cam.className = 'Camera';
this.videoMediaContainer.appendChild(cam);
this.removeVideoPinMediaContainer();
setColor(btnPn, 'white');
}
handleAspectRatio();
});
}
}
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';
});
}
}
}
// ####################################################
// REMOVE VIDEO PIN MEDIA CONTAINER
// ####################################################
removeVideoPinMediaContainer() {
this.videoPinMediaContainer.style.display = 'none';
this.videoMediaContainer.style.top = 0;
this.videoMediaContainer.style.right = null;
this.videoMediaContainer.style.width = '100%';
this.videoMediaContainer.style.height = '100%';
this.pinnedVideoPlayerId = null;
this.isVideoPinned = false;
if (this.isChatPinned) {
this.chatPin();
}
if (this.transcription.isPin()) {
this.transcription.pinned();
}
}
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');
});
}
}
// ####################################################
// VIDEO CIRCLE - PRIVACY MODE
// ####################################################
handleVP(elemId, vpId) {
let videoPlayer = this.getId(elemId);
let btnVp = this.getId(vpId);
if (btnVp && videoPlayer) {
btnVp.addEventListener('click', () => {
this.sound('click');
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)[0];
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('all', 'all');
}
isParticipantsListOpen = !isParticipantsListOpen;
this.isChatOpen = !this.isChatOpen;
if (this.isChatPinned) this.chatUnpin();
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');
}
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);
}
}
chatPin() {
if (!this.isVideoPinned) {
this.videoMediaContainer.style.top = 0;
this.videoMediaContainer.style.width = '75%';
this.videoMediaContainer.style.height = '100%';
}
this.chatPinned();
this.isChatPinned = true;
setColor(chatTogglePin, 'lime');
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.videoMediaContainer.style.top = 0;
this.videoMediaContainer.style.right = null;
this.videoMediaContainer.style.width = '100%';
this.videoMediaContainer.style.height = '100%';
}
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');
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) {
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_id: this.peer_id,
to_peer_id: 'ChatGPT',
to_peer_name: 'ChatGPT',
peer_msg: peer_msg,
};
if (isChatGPTOn) {
console.log('Send message:', data);
this.socket.emit('message', data);
this.setMsgAvatar('left', this.peer_name);
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.speechInMessages ? this.speechMessage(true, 'ChatGPT', message) : this.sound('message');
})
.catch((err) => {
console.log('ChatGPT error:', err);
});
} else {
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.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_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.appendMessage(
'left',
this.leftMsgAvatar,
this.peer_name,
this.peer_id,
peer_msg,
to_peer_id,
toPeerName,
);
if (!this.isChatOpen) this.toggleChat();
}
});
}
async showMessage(data) {
if (!this.isChatOpen && this.showChatOnMessage) await this.toggleChat();
this.setMsgAvatar('right', data.peer_name);
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');
}
this.speechInMessages ? this.speechMessage(true, data.peer_name, data.peer_msg) : 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'].includes(data.to_peer_id)) {
this.getId(`${data.peer_id}-unread-msg`).classList.remove('hidden');
}
}
}
}
setMsgAvatar(avatar, peerName) {
let avatarImg = rc.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
? `<span class="message-data-time">${time}, ${getFromName} ( me ) </span>`
: `<span class="message-data-time">${time}, ${getFromName} </span>`;
const speechButton = this.isSpeechSynthesisSupported
? `<button
id="msg-speech-${chatMessagesId}"
class="mr5"
onclick="rc.speechMessage(false, '${getFromName}', '${this.formatMsg(getMsg)}')">
<i class="fas fa-volume-high"></i>
</button>`
: '';
const positionFirst = myMessage
? `<img src="${getImg}" alt="avatar" />${timeAndName}`
: `${timeAndName}<img src="${getImg}" alt="avatar" />`;
const message = getFromName === 'ChatGPT' ? `<pre>${getMsg}</pre>` : getMsg;
const newMessageHTML = `
<li id="msg-${chatMessagesId}"
data-from-id="${getFromId}"
data-from-name="${getFromName}"
data-to-id="${getToId}"
data-to-name="${getToName}"
class="clearfix"
>
<div class="message-data ${messageData}">
${positionFirst}
</div>
<div class="message ${messageClass}">
<span class="text-start " id="${chatMessagesId}">${message}</span>
<hr/>
<div class="about-buttons mt5">
<button
id="msg-copy-${chatMessagesId}"
class="mr5"
onclick="rc.copyToClipboard('${chatMessagesId}')">
<i class="fas fa-paste"></i>
</button>
${speechButton}
<button
id="msg-delete-${chatMessagesId}"
class="mr5"
onclick="rc.deleteMessage('msg-${chatMessagesId}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</li>
`;
this.collectMessages(time, getFromName, getMsg);
console.log('Append message to:', { to_id: getToId, to_name: getToName });
switch (getToId) {
case 'ChatGPT':
chatGPTMessages.insertAdjacentHTML('beforeend', newMessageHTML);
break;
case 'all':
chatPublicMessages.insertAdjacentHTML('beforeend', newMessageHTML);
break;
default:
chatPrivateMessages.insertAdjacentHTML('beforeend', newMessageHTML);
break;
}
chatHistory.scrollTop += 500;
if (!this.isMobileDevice) {
this.setTippy('msg-delete-' + chatMessagesId, 'Delete', 'top');
this.setTippy('msg-copy-' + chatMessagesId, 'Copy', 'top');
this.setTippy('msg-speech-' + chatMessagesId, 'Speech', 'top');
}
chatMessagesId++;
}
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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
'/': '&#x2F;',
};
return input.replace(/[&<>"'/]/g, (m) => map[m]);
}
isHtml(str) {
var 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) {
const pattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$',
'i',
); // fragment locator
return pattern.test(input);
}
isImageURL(input) {
return input.match(/\.(jpeg|jpg|gif|png|tiff|bmp)$/) != null;
}
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);
}
speechText(msg) {
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(chatPublicMessages);
removeAllChildNodes(chatPrivateMessages);
this.chatMessages = [];
this.chatGPTContext = [];
this.sound('delete');
}
});
}
chatSave() {
if (this.chatMessages.length === 0) {
return userLog('info', 'No chat messages to save', 'top-end');
}
saveObjToJsonFile(this.chatMessages, 'CHAT');
}
// ####################################################
// RECORDING
// ####################################################
handleRecordingError(error, popupLog = true) {
console.error('Recording error', error);
if (popupLog) this.userLog('error', error, 'top-end', 6000);
}
getSupportedMimeTypes() {
const possibleTypes = ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/mp4'];
possibleTypes.splice(recPrioritizeH264 ? 0 : 2, 0, 'video/mp4;codecs=h264,aac', 'video/webm;codecs=h264,opus');
console.log('POSSIBLE CODECS', possibleTypes);
return possibleTypes.filter((mimeType) => {
return MediaRecorder.isTypeSupported(mimeType);
});
}
startRecording() {
recordedBlobs = [];
// Get supported MIME types and set options
const supportedMimeTypes = this.getSupportedMimeTypes();
console.log('MediaRecorder supported options', supportedMimeTypes);
const options = { mimeType: supportedMimeTypes[0] };
recCodecs = supportedMimeTypes[0];
try {
this.audioRecorder = new MixedAudioRecorder();
const audioStreams = this.getAudioStreamFromAudioElements();
console.log('Audio streams tracks --->', audioStreams.getTracks());
const audioMixerStreams = this.audioRecorder.getMixedAudioStream(
audioStreams
.getTracks()
.filter((track) => track.kind === 'audio')
.map((track) => new MediaStream([track])),
);
const audioMixerTracks = audioMixerStreams.getTracks();
console.log('Audio mixer tracks --->', audioMixerTracks);
this.isMobileDevice
? this.startMobileRecording(options, audioMixerTracks)
: this.recordingOptions(options, audioMixerTracks);
} catch (err) {
this.handleRecordingError('Exception while creating MediaRecorder: ' + err);
}
}
recordingOptions(options, audioMixerTracks) {
Swal.fire({
background: swalBackground,
position: 'top',
imageUrl: image.recording,
title: 'Recording options',
showDenyButton: true,
showCancelButton: true,
cancelButtonColor: 'red',
denyButtonColor: 'green',
confirmButtonText: `Camera`,
denyButtonText: `Screen/Window`,
cancelButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
this.startMobileRecording(options, audioMixerTracks);
} else if (result.isDenied) {
this.startDesktopRecording(options, audioMixerTracks);
}
});
}
startMobileRecording(options, audioMixerTracks) {
try {
// Combine audioMixerTracks and videoTracks into a single array
const combinedTracks = [];
if (Array.isArray(audioMixerTracks)) {
combinedTracks.push(...audioMixerTracks);
}
if (this.localVideoStream !== null) {
const videoTracks = this.localVideoStream.getVideoTracks();
console.log('Cam video tracks --->', videoTracks);
if (Array.isArray(videoTracks)) {
combinedTracks.push(...videoTracks);
}
}
const recCamStream = new MediaStream(combinedTracks);
console.log('New Cam Media Stream tracks --->', recCamStream.getTracks());
this.mediaRecorder = new MediaRecorder(recCamStream, options);
console.log('Created MediaRecorder', this.mediaRecorder, 'with options', options);
this.getId('swapCameraButton').className = 'hidden';
this.initRecording();
} catch (err) {
this.handleRecordingError('Unable to record the camera + audio: ' + err, false);
}
}
startDesktopRecording(options, audioMixerTracks) {
// On desktop devices, record camera or screen/window... + all audio tracks
const constraints = { video: true };
navigator.mediaDevices
.getDisplayMedia(constraints)
.then((screenStream) => {
const screenTracks = screenStream.getVideoTracks();
console.log('Screen video tracks --->', screenTracks);
const combinedTracks = [];
if (Array.isArray(screenTracks)) {
combinedTracks.push(...screenTracks);
}
if (Array.isArray(audioMixerTracks)) {
combinedTracks.push(...audioMixerTracks);
}
const recScreenStream = new MediaStream(combinedTracks);
console.log('New Screen/Window Media Stream tracks --->', recScreenStream.getTracks());
this.recScreenStream = recScreenStream;
this.mediaRecorder = new MediaRecorder(recScreenStream, options);
console.log('Created MediaRecorder', this.mediaRecorder, 'with options', options);
this.initRecording();
})
.catch((err) => {
this.handleRecordingError('Unable to record the screen + audio: ' + err, false);
});
}
initRecording() {
this._isRecording = true;
this.handleMediaRecorder();
this.event(_EVENTS.startRec);
this.recordingAction(enums.recording.start);
this.sound('recStart');
}
hasAudioTrack(mediaStream) {
if (!mediaStream) return false;
const audioTracks = mediaStream.getAudioTracks();
return audioTracks.length > 0;
}
hasVideoTrack(mediaStream) {
if (!mediaStream) return false;
const videoTracks = mediaStream.getVideoTracks();
return videoTracks.length > 0;
}
getAudioTracksFromAudioElements() {
const audioElements = document.querySelectorAll('audio');
const audioTracks = [];
audioElements.forEach((audio) => {
const audioTrack = audio.srcObject.getAudioTracks()[0];
if (audioTrack) {
audioTracks.push(audioTrack);
}
});
return audioTracks;
}
getAudioStreamFromAudioElements() {
const audioElements = document.querySelectorAll('audio');
const audioStream = new MediaStream();
audioElements.forEach((audio) => {
const audioTrack = audio.srcObject.getAudioTracks()[0];
if (audioTrack) {
audioStream.addTrack(audioTrack);
}
});
return audioStream;
}
handleMediaRecorder() {
if (this.mediaRecorder) {
this.recServerFileName = this.getServerRecFileName();
rc.recSyncServerRecording ? this.mediaRecorder.start(this.recSyncTime) : this.mediaRecorder.start();
this.mediaRecorder.addEventListener('start', this.handleMediaRecorderStart);
this.mediaRecorder.addEventListener('dataavailable', this.handleMediaRecorderData);
this.mediaRecorder.addEventListener('stop', this.handleMediaRecorderStop);
}
}
getServerRecFileName() {
const dateTime = getDataTimeStringFormat();
const roomName = this.room_id.trim();
return `Rec_${roomName}_${dateTime}.webm`;
}
handleMediaRecorderStart(evt) {
console.log('MediaRecorder started: ', evt);
rc.cleanLastRecordingInfo();
rc.disableRecordingOptions();
}
handleMediaRecorderData(evt) {
// console.log('MediaRecorder data: ', evt);
if (evt.data && evt.data.size > 0) {
rc.recSyncServerRecording ? rc.syncRecordingInCloud(evt.data) : recordedBlobs.push(evt.data);
}
}
async syncRecordingInCloud(data) {
const arrayBuffer = await data.arrayBuffer();
const chunkSize = rc.recSyncChunkSize;
const totalChunks = Math.ceil(arrayBuffer.byteLength / chunkSize);
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const chunk = arrayBuffer.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize);
try {
await axios.post('/recSync?fileName=' + rc.recServerFileName, chunk, {
headers: {
'Content-Type': 'application/octet-stream',
},
});
} catch (error) {
console.error('Error syncing chunk:', error.message);
}
}
}
handleMediaRecorderStop(evt) {
try {
console.log('MediaRecorder stopped: ', evt);
rc.recSyncServerRecording ? rc.handleServerRecordingStop() : rc.handleLocalRecordingStop();
rc.disableRecordingOptions(false);
} catch (err) {
console.error('Recording save failed', err);
}
}
disableRecordingOptions(disabled = true) {
switchH264Recording.disabled = disabled;
switchServerRecording.disabled = disabled;
switchHostOnlyRecording.disabled = disabled;
}
handleLocalRecordingStop() {
console.log('MediaRecorder Blobs: ', recordedBlobs);
const dateTime = getDataTimeString();
const type = recordedBlobs[0].type.includes('mp4') ? 'mp4' : 'webm';
const blob = new Blob(recordedBlobs, { type: 'video/' + type });
const recFileName = `Rec_${dateTime}.${type}`;
const currentDevice = DetectRTC.isMobileDevice ? 'MOBILE' : 'PC';
const blobFileSize = bytesToSize(blob.size);
const recTime = document.getElementById('recordingStatus');
const recType = 'Locally';
const recordingInfo = `
<br/><br/>
<ul>
<li>Stored: ${recType}</li>
<li>Time: ${recTime.innerText}</li>
<li>File: ${recFileName}</li>
<li>Codecs: ${recCodecs}</li>
<li>Size: ${blobFileSize}</li>
</ul>
<br/>
`;
const recordingMsg = `Please wait to be processed, then will be downloaded to your ${currentDevice} device.`;
this.saveLastRecordingInfo(recordingInfo);
this.showRecordingInfo(recType, recordingInfo, recordingMsg);
this.saveRecordingInLocalDevice(blob, recFileName, recTime);
}
handleServerRecordingStop() {
console.log('MediaRecorder Stop');
const recTime = document.getElementById('recordingStatus');
const recType = 'Server';
const recordingInfo = `
<br/><br/>
<ul>
<li>Stored: ${recType}</li>
<li>Time: ${recTime.innerText}</li>
<li>File: ${this.recServerFileName}</li>
<li>Codecs: ${recCodecs}</li>
</ul>
<br/>
`;
this.saveLastRecordingInfo(recordingInfo);
this.showRecordingInfo(recType, recordingInfo);
}
saveLastRecordingInfo(recordingInfo) {
const lastRecordingInfo = document.getElementById('lastRecordingInfo');
lastRecordingInfo.style.color = '#FFFFFF';
lastRecordingInfo.innerHTML = `Last Recording Info: ${recordingInfo}`;
show(lastRecordingInfo);
}
cleanLastRecordingInfo() {
const lastRecordingInfo = document.getElementById('lastRecordingInfo');
lastRecordingInfo.innerHTML = '';
hide(lastRecordingInfo);
}
showRecordingInfo(recType, recordingInfo, recordingMsg = '') {
if (window.localStorage.isReconnected === 'false') {
Swal.fire({
background: swalBackground,
position: 'center',
icon: 'success',
title: 'Recording',
html: `<div style="text-align: left;">
🔴 ${recType} Recording Info:
${recordingInfo}
${recordingMsg}
</div>`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
}
saveRecordingInLocalDevice(blob, recFileName, recTime) {
console.log('MediaRecorder Download Blobs');
const url = window.URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.style.display = 'none';
downloadLink.href = url;
downloadLink.download = recFileName;
document.body.appendChild(downloadLink);
downloadLink.click();
setTimeout(() => {
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(url);
console.log(`🔴 Recording FILE: ${recFileName} done 👍`);
recordedBlobs = [];
recTime.innerText = '0s';
}, 100);
}
pauseRecording() {
if (this.mediaRecorder) {
this._isRecording = false;
this.mediaRecorder.pause();
this.event(_EVENTS.pauseRec);
this.recordingAction('Pause recording');
}
}
resumeRecording() {
if (this.mediaRecorder) {
this._isRecording = true;
this.mediaRecorder.resume();
this.event(_EVENTS.resumeRec);
this.recordingAction('Resume recording');
}
}
stopRecording() {
if (this.mediaRecorder) {
this._isRecording = false;
this.mediaRecorder.stop();
this.mediaRecorder = null;
if (this.recScreenStream) {
this.recScreenStream.getTracks().forEach((track) => {
if (track.kind === 'video') track.stop();
});
}
if (this.isMobileDevice) this.getId('swapCameraButton').className = '';
this.event(_EVENTS.stopRec);
this.audioRecorder.stopMixedAudioStream();
this.recordingAction(enums.recording.stop);
this.sound('recStop');
}
}
recordingAction(action) {
if (!this.thereAreParticipants()) return;
this.socket.emit('recordingAction', {
peer_name: this.peer_name,
peer_id: this.peer_id,
action: action,
});
}
handleRecordingAction(data) {
console.log('Handle recording action', data);
const { peer_name, peer_id, action } = data;
const recAction = {
side: 'left',
img: this.leftMsgAvatar,
peer_name: peer_name,
peer_id: peer_id,
peer_msg: `🔴 ${action}`,
to_peer_id: 'all',
to_peer_name: 'all',
};
this.showMessage(recAction);
const recData = {
type: 'recording',
action: action,
peer_name: peer_name,
};
this.msgHTML(
recData,
null,
image.recording,
null,
`${icons.user} ${peer_name}
<br /><br />
<span>🔴 ${action}</span>
<br />`,
);
}
saveRecording(reason) {
if (this._isRecording || recordingStatus.innerText != '0s') {
console.log(`Save recording: ${reason}`);
this.stopRecording();
}
}
// ####################################################
// FILE SHARING
// ####################################################
handleSF(uid) {
const words = uid.split('___');
let peer_id = words[1];
let btnSf = this.getId(uid);
if (btnSf) {
btnSf.addEventListener('click', () => {
this.selectFileToShare(peer_id);
});
}
}
handleDD(uid, peer_id, itsMe = false) {
let videoPlayer = this.getId(uid);
if (videoPlayer) {
videoPlayer.addEventListener('dragover', function (e) {
e.preventDefault();
});
videoPlayer.addEventListener('drop', function (e) {
e.preventDefault();
if (itsMe) {
return userLog('warning', 'You cannot send files to yourself.', 'top-end');
}
if (this.sendInProgress) {
return userLog('warning', 'Please wait for the previous file to be sent.', 'top-end');
}
if (e.dataTransfer.items && e.dataTransfer.items.length > 1) {
return userLog('warning', 'Please drag and drop a single file.', 'top-end');
}
if (e.dataTransfer.items) {
let item = e.dataTransfer.items[0].webkitGetAsEntry();
console.log('Drag and drop', item);
if (item.isDirectory) {
return userLog('warning', 'Please drag and drop a single file not a folder.', 'top-end');
}
var file = e.dataTransfer.items[0].getAsFile();
rc.sendFileInformations(file, peer_id);
} else {
rc.sendFileInformations(e.dataTransfer.files[0], peer_id);
}
});
}
}
selectFileToShare(peer_id, broadcast = false) {
this.sound('open');
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
imageAlt: 'mirotalksfu-file-sharing',
imageUrl: image.share,
position: 'center',
title: 'Share file',
input: 'file',
inputAttributes: {
accept: this.fileSharingInput,
'aria-label': 'Select file',
},
showDenyButton: true,
confirmButtonText: `Send`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
this.sendFileInformations(result.value, peer_id, broadcast);
}
});
}
sendFileInformations(file, peer_id, broadcast = false) {
this.fileToSend = file;
//
if (this.fileToSend && this.fileToSend.size > 0) {
if (!this.thereAreParticipants()) {
return userLog('info', 'No participants detected', 'top-end');
}
// prevent XSS injection
if (this.isHtml(this.fileToSend.name) || !this.isValidFileName(this.fileToSend.name))
return userLog('warning', 'Invalid file name!', 'top-end', 5000);
const fileInfo = {
peer_id: peer_id,
broadcast: broadcast,
peer_name: this.peer_name,
fileName: this.fileToSend.name,
fileSize: this.fileToSend.size,
fileType: this.fileToSend.type,
};
this.setMsgAvatar('left', this.peer_name);
this.appendMessage(
'left',
this.leftMsgAvatar,
this.peer_name,
this.peer_id,
`${icons.fileSend} File send:
<br/>
<ul>
<li>Name: ${this.fileToSend.name}</li>
<li>Size: ${this.bytesToSize(this.fileToSend.size)}</li>
</ul>`,
'all',
'all',
);
// send some metadata about our file to peers in the room
this.socket.emit('fileInfo', fileInfo);
setTimeout(() => {
this.sendFileData(peer_id, broadcast);
}, 1000);
} else {
userLog('error', 'File not selected or empty.', 'top-end');
}
}
handleFileInfo(data) {
this.incomingFileInfo = data;
this.incomingFileData = [];
this.receiveBuffer = [];
this.receivedSize = 0;
let fileToReceiveInfo =
' From: ' +
this.incomingFileInfo.peer_name +
html.newline +
' Incoming file: ' +
this.incomingFileInfo.fileName +
html.newline +
' File type: ' +
this.incomingFileInfo.fileType +
html.newline +
' File size: ' +
this.bytesToSize(this.incomingFileInfo.fileSize);
this.setMsgAvatar('right', this.incomingFileInfo.peer_name);
this.appendMessage(
'right',
this.rightMsgAvatar,
this.incomingFileInfo.peer_name,
this.incomingFileInfo.peer_id,
`${icons.fileReceive} File receive:
<br/>
<ul>
<li>From: ${this.incomingFileInfo.peer_name}</li>
<li>Name: ${this.incomingFileInfo.fileName}</li>
<li>Size: ${this.bytesToSize(this.incomingFileInfo.fileSize)}</li>
</ul>`,
'all',
'all',
);
receiveFileInfo.innerText = fileToReceiveInfo;
receiveFileDiv.style.display = 'inline';
receiveProgress.max = this.incomingFileInfo.fileSize;
this.userLog('info', fileToReceiveInfo, 'top-end');
this.receiveInProgress = true;
}
sendFileData(peer_id, broadcast) {
console.log('Send file ', {
name: this.fileToSend.name,
size: this.bytesToSize(this.fileToSend.size),
type: this.fileToSend.type,
});
this.sendInProgress = true;
sendFileInfo.innerText =
'File name: ' +
this.fileToSend.name +
html.newline +
'File type: ' +
this.fileToSend.type +
html.newline +
'File size: ' +
this.bytesToSize(this.fileToSend.size) +
html.newline;
sendFileDiv.style.display = 'inline';
sendProgress.max = this.fileToSend.size;
this.fileReader = new FileReader();
let offset = 0;
this.fileReader.addEventListener('error', (err) => console.error('fileReader error', err));
this.fileReader.addEventListener('abort', (e) => console.log('fileReader aborted', e));
this.fileReader.addEventListener('load', (e) => {
if (!this.sendInProgress) return;
let data = {
peer_id: peer_id,
broadcast: broadcast,
fileData: e.target.result,
};
this.sendFSData(data);
offset += data.fileData.byteLength;
sendProgress.value = offset;
sendFilePercentage.innerText = 'Send progress: ' + ((offset / this.fileToSend.size) * 100).toFixed(2) + '%';
// send file completed
if (offset === this.fileToSend.size) {
this.sendInProgress = false;
sendFileDiv.style.display = 'none';
userLog('success', 'The file ' + this.fileToSend.name + ' was sent successfully.', 'top-end');
}
if (offset < this.fileToSend.size) readSlice(offset);
});
const readSlice = (o) => {
const slice = this.fileToSend.slice(offset, o + this.chunkSize);
this.fileReader.readAsArrayBuffer(slice);
};
readSlice(0);
}
sendFSData(data) {
if (data) this.socket.emit('file', data);
}
abortFileTransfer() {
if (this.fileReader && this.fileReader.readyState === 1) {
this.fileReader.abort();
sendFileDiv.style.display = 'none';
this.sendInProgress = false;
this.socket.emit('fileAbort', {
peer_name: this.peer_name,
});
}
}
hideFileTransfer() {
receiveFileDiv.style.display = 'none';
}
handleFileAbort(data) {
this.receiveBuffer = [];
this.incomingFileData = [];
this.receivedSize = 0;
this.receiveInProgress = false;
receiveFileDiv.style.display = 'none';
console.log(data.peer_name + ' aborted the file transfer');
userLog('info', data.peer_name + ' aborted the file transfer', 'top-end');
}
handleFile(data) {
if (!this.receiveInProgress) return;
this.receiveBuffer.push(data.fileData);
this.receivedSize += data.fileData.byteLength;
receiveProgress.value = this.receivedSize;
receiveFilePercentage.innerText =
'Receive progress: ' + ((this.receivedSize / this.incomingFileInfo.fileSize) * 100).toFixed(2) + '%';
if (this.receivedSize === this.incomingFileInfo.fileSize) {
receiveFileDiv.style.display = 'none';
this.incomingFileData = this.receiveBuffer;
this.receiveBuffer = [];
this.endFileDownload();
}
}
endFileDownload() {
this.sound('download');
// save received file into Blob
const blob = new Blob(this.incomingFileData);
const file = this.incomingFileInfo.fileName;
this.incomingFileData = [];
// if file is image, show the preview
if (isImageURL(this.incomingFileInfo.fileName)) {
const reader = new FileReader();
reader.onload = (e) => {
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
position: 'center',
title: 'Received file',
text: this.incomingFileInfo.fileName + ' size ' + this.bytesToSize(this.incomingFileInfo.fileSize),
imageUrl: e.target.result,
imageAlt: 'mirotalksfu-file-img-download',
showDenyButton: true,
confirmButtonText: `Save`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) this.saveBlobToFile(blob, file);
});
};
// blob where is stored downloaded file
reader.readAsDataURL(blob);
} else {
// not img file
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
position: 'center',
title: 'Received file',
text: this.incomingFileInfo.fileName + ' size ' + this.bytesToSize(this.incomingFileInfo.fileSize),
showDenyButton: true,
confirmButtonText: `Save`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) this.saveBlobToFile(blob, file);
});
}
}
saveBlobToFile(blob, file) {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = file;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
bytesToSize(bytes) {
let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0 Byte';
let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
}
toHtmlJson(obj) {
return '<pre>' + JSON.stringify(obj, null, 4) + '</pre>';
}
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') {
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);
}
}
});
}
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) {
let peer_name = data.peer_name;
let action = data.action;
switch (action) {
case 'open':
this.userLog('info', `${peer_name} <i class="fab fa-youtube"></i> opened the video`, 'top-end');
this.openVideo(data);
break;
case 'close':
this.userLog('info', `${peer_name} <i class="fab fa-youtube"></i> closed the video`, 'top-end');
this.closeVideo();
break;
default:
break;
}
}
openVideo(data) {
let d, vb, e, video, pn;
let peer_name = data.peer_name;
let video_url = data.video_url;
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 = 'videoMenuBar fadein';
e = document.createElement('button');
e.className = 'fas fa-times';
e.id = '__videoExit';
pn = document.createElement('button');
pn.id = '__pinUnpin';
pn.className = html.pin;
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);
} 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);
if (!this.isMobileDevice) vb.appendChild(pn);
d.appendChild(video);
d.appendChild(vb);
this.videoMediaContainer.appendChild(d);
handleAspectRatio();
let exitVideoBtn = this.getId(e.id);
exitVideoBtn.addEventListener('click', (e) => {
e.preventDefault();
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');
}
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':
this.sound('locked');
this.event(_EVENTS.roomLock);
this.userLog('info', `${icons.lock} LOCKED the room by the password`, 'top-end');
break;
case 'unlock':
this.event(_EVENTS.roomUnlock);
this.userLog('info', `${icons.unlock} UNLOCKED the room`, 'top-end');
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 '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
? userLog('info', `${icons.chat} Chat will be shown, when you receive a message`, 'top-end')
: 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 'transcriptIsPersistentMode':
userLog('info', `${icons.transcript} Persistent transcription mode active: ${active}`, 'top-end');
break;
case 'transcriptShowOnMsg':
active
? userLog(
'info',
`${icons.transcript} Transcript will be shown, when you receive a message`,
'top-end',
)
: userLog(
'info',
`${icons.transcript} Transcript not will be shown, when you receive a message`,
'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 'disconnect_all_on_leave':
this.userLog('info', `${icons.moderator} Moderator: disconnect all on leave room ${status}`, 'top-end');
break;
case 'recPrioritizeH264':
this.userLog('info', `${icons.codecs} Recording prioritize h.264 ${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;
default:
break;
}
}
roomPassword(data) {
switch (data.password) {
case 'OK':
this.joinAllowed(data.room);
break;
case 'KO':
this.roomIsLocked();
break;
default:
break;
}
}
// ####################################################
// ROOM LOBBY
// ####################################################
async roomLobby(data) {
console.log('LOBBY--->', data);
switch (data.lobby_status) {
case 'waiting':
if (!isRulesActive || isPresenter) {
let lobbyTr = '';
let peer_id = data.peer_id;
let peer_name = data.peer_name;
let avatarImg = rc.isValidEmail(peer_name)
? this.genGravatar(peer_name)
: this.genAvatarSvg(peer_name, 32);
let lobbyTb = this.getId('lobbyTb');
let lobbyAccept = _PEER.acceptPeer;
let lobbyReject = _PEER.ejectPeer;
let lobbyAcceptId = `${peer_name}___${peer_id}___lobbyAccept`;
let lobbyRejectId = `${peer_name}___${peer_id}___lobbyReject`;
lobbyTr += `
<tr id='${peer_id}'>
<td><img src="${avatarImg}" /></td>
<td>${peer_name}</td>
<td><button id='${lobbyAcceptId}' onclick="rc.lobbyAction(this.id, 'accept')">${lobbyAccept}</button></td>
<td><button id='${lobbyRejectId}' onclick="rc.lobbyAction(this.id, 'reject')">${lobbyReject}</button></td>
</tr>
`;
lobbyTb.innerHTML += lobbyTr;
lobbyParticipantsCount++;
lobbyHeaderTitle.innerText = 'Lobby users (' + lobbyParticipantsCount + ')';
if (!isLobbyOpen) this.lobbyToggle();
if (!this.isMobileDevice) {
setTippy(lobbyAcceptId, 'Accept', 'top');
setTippy(lobbyRejectId, 'Reject', 'top');
}
this.userLog('info', peer_name + ' wants to join the meeting', 'top-end');
}
break;
case 'accept':
await this.joinAllowed(data.room);
control.style.display = 'flex';
this.msgPopup('info', 'Your join meeting was be accepted by moderator');
break;
case 'reject':
this.sound('eject');
Swal.fire({
icon: 'warning',
allowOutsideClick: false,
allowEscapeKey: true,
showDenyButton: false,
showConfirmButton: true,
background: swalBackground,
title: 'Rejected',
text: 'Your join meeting was be rejected by 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;
}
}
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: false,
};
this.socket.emit('roomLobby', data);
const trElem = this.getId(peer_id);
trElem.parentNode.removeChild(trElem);
lobbyParticipantsCount--;
lobbyHeaderTitle.innerText = 'Lobby users (' + lobbyParticipantsCount + ')';
if (lobbyParticipantsCount == 0) this.lobbyToggle();
}
lobbyAcceptAll() {
if (lobbyParticipantsCount > 0) {
const data = this.lobbyGetData('accept', this.lobbyGetPeerIds());
this.socket.emit('roomLobby', data);
this.lobbyRemoveAll();
} else {
this.userLog('info', 'No participants in lobby detected', 'top-end');
}
}
lobbyRejectAll() {
if (lobbyParticipantsCount > 0) {
const data = this.lobbyGetData('reject', this.lobbyGetPeerIds());
this.socket.emit('roomLobby', data);
this.lobbyRemoveAll();
} else {
this.userLog('info', 'No participants in lobby detected', 'top-end');
}
}
lobbyRemoveAll() {
let tr = lobbyTb.getElementsByTagName('tr');
for (let i = tr.length - 1; i >= 0; i--) {
if (tr[i].id && tr[i].id != 'lobbyAll') {
console.log('REMOVE LOBBY PEER ID ' + tr[i].id);
if (tr[i] && tr[i].parentElement) {
tr[i].parentElement.removeChild(tr[i]);
}
lobbyParticipantsCount--;
}
}
lobbyHeaderTitle.innerText = 'Lobby users (' + lobbyParticipantsCount + ')';
if (lobbyParticipantsCount == 0) this.lobbyToggle();
}
lobbyRemoveMe(peer_id) {
let tr = lobbyTb.getElementsByTagName('tr');
for (let i = tr.length - 1; i >= 0; i--) {
if (tr[i].id && tr[i].id == peer_id) {
console.log('REMOVE LOBBY PEER ID ' + tr[i].id);
if (tr[i] && tr[i].parentElement) {
tr[i].parentElement.removeChild(tr[i]);
}
lobbyParticipantsCount--;
}
}
lobbyHeaderTitle.innerText = 'Lobby users (' + lobbyParticipantsCount + ')';
if (lobbyParticipantsCount == 0) this.lobbyToggle();
}
lobbyGetPeerIds() {
let peers_id = [];
let tr = lobbyTb.getElementsByTagName('tr');
for (let i = tr.length - 1; i >= 0; i--) {
if (tr[i].id && tr[i].id != 'lobbyAll') {
peers_id.push(tr[i].id);
}
}
return peers_id;
}
lobbyGetData(status, peers_id = []) {
return {
room_id: this.room_id,
peer_id: this.peer_id,
peer_name: this.peer_name,
peers_id: peers_id,
lobby_status: status,
broadcast: true,
};
}
lobbyToggle() {
if (lobbyParticipantsCount > 0 && !isLobbyOpen) {
lobby.style.display = 'block';
lobby.style.top = '50%';
lobby.style.left = '50%';
if (this.isMobileDevice) {
lobby.style.width = '100%';
lobby.style.height = '100%';
}
isLobbyOpen = true;
this.sound('lobby');
} else {
lobby.style.display = 'none';
isLobbyOpen = false;
}
}
// ####################################################
// HANDLE ROOM ACTION
// ####################################################
userUnauthorized() {
this.sound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
imageUrl: image.forbidden,
title: 'Oops, Unauthorized',
text: 'The host has user authentication enabled',
confirmButtonText: `Login`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then(() => {
// Login required to join room
openURL(`/login/?room=${this.room_id}`);
});
}
unlockTheRoom() {
if (room_password) {
this.RoomPassword = room_password;
let data = {
action: 'checkPassword',
password: this.RoomPassword,
};
this.socket.emit('roomAction', data);
} else {
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
imageUrl: image.locked,
title: 'Oops, Room is Locked',
input: 'text',
inputPlaceholder: 'Enter the Room password',
confirmButtonText: `OK`,
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(() => {
let data = {
action: 'checkPassword',
password: this.RoomPassword,
};
this.socket.emit('roomAction', data);
});
}
}
roomIsLocked() {
this.sound('eject');
this.event(_EVENTS.roomLock);
console.log('Room is Locked, try with another one');
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
position: 'center',
imageUrl: image.locked,
title: 'Oops, Wrong Room Password',
text: 'The room is locked, 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) this.exit();
});
}
waitJoinConfirm() {
this.sound('lobby');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: true,
showConfirmButton: false,
background: swalBackground,
imageUrl: image.poster,
title: 'Room has lobby enabled',
text: 'Asking to join meeting...',
confirmButtonText: `Ok`,
denyButtonText: `Leave room`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
control.style.display = 'none';
} else {
this.exit();
}
});
}
isBanned() {
this.sound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: false,
showConfirmButton: true,
background: swalBackground,
imageUrl: image.forbidden,
title: 'Banned',
text: 'You are banned from this room!',
confirmButtonText: `Ok`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then(() => {
this.exit();
});
}
// ####################################################
// HANDLE AUDIO VOLUME
// ####################################################
handleAudioVolume(data) {
if (!isPitchBarEnabled) return;
let peerId = data.peer_id;
let peerName = data.peer_name;
let producerAudioBtn = this.getId(peerId + '_audio');
let consumerAudioBtn = this.getId(peerId + '__audio');
let pbProducer = this.getId(peerId + '_pitchBar');
let pbConsumer = this.getId(peerId + '__pitchBar');
let audioVolume = data.audioVolume * 10; //10-100
let audioColor = 'lime';
//console.log('Active speaker', { peer_name: peerName, peer_id: peerId, audioVolume: audioVolume });
if ([50, 60, 70].includes(audioVolume)) audioColor = 'orange';
if ([80, 90, 100].includes(audioVolume)) audioColor = 'red';
if (producerAudioBtn) producerAudioBtn.style.color = audioColor;
if (consumerAudioBtn) consumerAudioBtn.style.color = audioColor;
if (pbProducer) pbProducer.style.backgroundColor = audioColor;
if (pbConsumer) pbConsumer.style.backgroundColor = audioColor;
if (pbProducer) pbProducer.style.height = audioVolume + '%';
if (pbConsumer) pbConsumer.style.height = audioVolume + '%';
setTimeout(function () {
audioColor = 'white';
if (producerAudioBtn) producerAudioBtn.style.color = audioColor;
if (consumerAudioBtn) consumerAudioBtn.style.color = audioColor;
if (pbProducer) pbProducer.style.height = '0%';
if (pbConsumer) pbConsumer.style.height = '0%';
}, 200);
}
// ####################################################
// HANDLE PEER VOLUME
// ###################################################
handlePV(uid) {
const words = uid.split('___');
let peer_id = words[1] + '___pVolume';
let audioConsumerId = this.audioConsumers.get(peer_id);
let audioConsumerPlayer = this.getId(audioConsumerId);
let inputPv = this.getId(peer_id);
if (inputPv && audioConsumerPlayer) {
inputPv.style.display = 'inline';
inputPv.value = 100;
// Not work on Mobile?
inputPv.addEventListener('input', () => {
audioConsumerPlayer.volume = inputPv.value / 100;
});
}
}
// ####################################################
// HANDLE BAN
// ###################################################
handleGL(uid) {
const words = uid.split('___');
let peer_id = words[1] + '___pGeoLocation';
let btnGl = this.getId(uid);
if (btnGl) {
btnGl.addEventListener('click', () => {
isPresenter
? this.askPeerGeoLocation(peer_id)
: this.userLog('warning', 'Only the presenter can ask geolocation to the participants', 'top-end');
});
}
}
// ####################################################
// HANDLE BAN
// ###################################################
handleBAN(uid) {
const words = uid.split('___');
let peer_id = words[1] + '___pBan';
let btnBan = this.getId(uid);
if (btnBan) {
btnBan.addEventListener('click', () => {
isPresenter
? this.peerAction('me', peer_id, 'ban')
: this.userLog('warning', 'Only the presenter can ban the participants', 'top-end');
});
}
}
// ####################################################
// HANDLE KICK-OUT
// ###################################################
handleKO(uid) {
const words = uid.split('___');
let peer_id = words[1] + '___pEject';
let btnKo = this.getId(uid);
if (btnKo) {
btnKo.addEventListener('click', () => {
isPresenter
? this.peerAction('me', peer_id, 'eject')
: this.userLog('warning', 'Only the presenter can eject the participants', 'top-end');
});
}
}
// ####################################################
// HANDLE VIDEO
// ###################################################
handleCM(uid) {
const words = uid.split('___');
let peer_id = words[1] + '___pVideo';
let btnCm = this.getId(uid);
if (btnCm) {
btnCm.addEventListener('click', (e) => {
if (e.target.className === html.videoOn) {
isPresenter
? this.peerAction('me', peer_id, 'hide')
: this.userLog('warning', 'Only the presenter can hide the participants', 'top-end');
} else {
isPresenter
? this.peerAction('me', peer_id, 'unhide')
: this.userLog('warning', 'Only the presenter can unhide the participants', 'top-end');
}
});
}
}
// ####################################################
// HANDLE AUDIO
// ###################################################
handleAU(uid) {
const words = uid.split('__');
let peer_id = words[0] + '___pAudio';
let btnAU = this.getId(uid);
if (btnAU) {
btnAU.addEventListener('click', (e) => {
if (e.target.className === html.audioOn) {
isPresenter
? this.peerAction('me', peer_id, 'mute')
: this.userLog('warning', 'Only the presenter can mute the participants', 'top-end');
} else {
isPresenter
? this.peerAction('me', peer_id, 'unmute')
: this.userLog('warning', 'Only the presenter can unmute the participants', 'top-end');
}
});
}
}
// ####################################################
// HANDLE COMMANDS
// ####################################################
emitCmd(cmd) {
this.socket.emit('cmd', cmd);
}
handleCmd(cmd) {
switch (cmd.type) {
case 'privacy':
this.setVideoPrivacyStatus(cmd.peer_id, cmd.active);
break;
case 'roomEmoji':
this.handleRoomEmoji(cmd);
break;
case 'transcript':
this.transcription.handleTranscript(cmd);
break;
case 'geoLocation':
this.confirmPeerGeoLocation(cmd);
break;
case 'geoLocationOK':
this.handleGeoPeerLocation(cmd);
break;
case 'geoLocationKO':
this.sound('alert');
this.userLog('warning', cmd.data, 'top-end', 5000);
break;
case 'ejectAll':
this.exit();
break;
default:
break;
//...
}
}
handleRoomEmoji(cmd, duration = 5000) {
const userEmoji = document.getElementById(`userEmoji`);
if (userEmoji) {
const emojiDisplay = document.createElement('div');
emojiDisplay.className = 'animate__animated animate__backInUp';
emojiDisplay.style.padding = '10px';
emojiDisplay.style.fontSize = '3vh';
emojiDisplay.style.color = '#FFF';
emojiDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
emojiDisplay.style.borderRadius = '10px';
emojiDisplay.innerText = `${cmd.emoji} ${cmd.peer_name}`;
userEmoji.appendChild(emojiDisplay);
setTimeout(() => {
emojiDisplay.remove();
}, duration);
}
}
// ####################################################
// PEER ACTION
// ####################################################
async peerAction(from_peer_name, id, action, emit = true, broadcast = false, info = true, msg = '') {
const words = id.split('___');
const peer_id = words[0];
if (emit) {
// send...
const data = {
from_peer_name: this.peer_name,
from_peer_id: this.peer_id,
from_peer_uuid: this.peer_uuid,
to_peer_uuid: '',
peer_id: peer_id,
action: action,
message: '',
broadcast: broadcast,
};
console.log('peerAction', data);
if (!this.thereAreParticipants()) {
if (info) return this.userLog('info', 'No participants detected', 'top-end');
}
if (!broadcast) {
switch (action) {
case 'mute':
const audioMessage =
'The participant has been muted, and only they have the ability to unmute themselves';
if (isBroadcastingEnabled) {
const peerAudioButton = this.getId(data.peer_id + '___pAudio');
if (peerAudioButton) {
const peerAudioIcon = peerAudioButton.querySelector('i');
if (peerAudioIcon && peerAudioIcon.style.color == 'red') {
if (isRulesActive && isPresenter) {
data.action = 'unmute';
return this.confirmPeerAction(data.action, data);
}
return this.userLog('info', audioMessage, 'top-end');
}
}
} else {
const peerAudioStatus = this.getId(data.peer_id + '__audio');
if (!peerAudioStatus || peerAudioStatus.className == html.audioOff) {
if (isRulesActive && isPresenter) {
data.action = 'unmute';
return this.confirmPeerAction(data.action, data);
}
return this.userLog('info', audioMessage, 'top-end');
}
}
break;
case 'hide':
const videoMessage =
'The participant is currently hidden, and only they have the option to unhide themselves';
if (isBroadcastingEnabled) {
const peerVideoButton = this.getId(data.peer_id + '___pVideo');
if (peerVideoButton) {
const peerVideoIcon = peerVideoButton.querySelector('i');
if (peerVideoIcon && peerVideoIcon.style.color == 'red') {
if (isRulesActive && isPresenter) {
data.action = 'unhide';
return this.confirmPeerAction(data.action, data);
}
return this.userLog('info', videoMessage, 'top-end');
}
}
} else {
const peerVideoOff = this.getId(data.peer_id + '__videoOff');
if (peerVideoOff) {
if (isRulesActive && isPresenter) {
data.action = 'unhide';
return this.confirmPeerAction(data.action, data);
}
return this.userLog('info', videoMessage, 'top-end');
}
}
case 'stop':
const screenMessage =
'The participant screen is not shared, only the participant can initiate sharing';
const peerScreenButton = this.getId(id);
if (peerScreenButton) {
const peerScreenStatus = peerScreenButton.querySelector('i');
if (peerScreenStatus && peerScreenStatus.style.color == 'red') {
if (isRulesActive && isPresenter) {
data.action = 'start';
return this.confirmPeerAction(data.action, data);
}
return this.userLog('info', screenMessage, 'top-end');
}
}
break;
case 'ban':
if (!isRulesActive || isPresenter) {
const peer_info = await getRemotePeerInfo(peer_id);
console.log('BAN PEER', peer_info);
if (peer_info) {
data.to_peer_uuid = peer_info.peer_uuid;
return this.confirmPeerAction(data.action, data);
}
}
break;
default:
break;
}
}
this.confirmPeerAction(data.action, data);
} else {
// receive...
const peerActionAllowed = peer_id === this.peer_id || broadcast;
switch (action) {
case 'ban':
if (peerActionAllowed) {
const message = `Will ban you from the room${
msg ? `<br><br><span class="red">Reason: ${msg}</span>` : ''
}`;
this.exit(true);
this.sound(action);
this.peerActionProgress(from_peer_name, message, 5000, action);
}
break;
case 'eject':
if (peerActionAllowed) {
const message = `Will eject you from the room${
msg ? `<br><br><span class="red">Reason: ${msg}</span>` : ''
}`;
this.exit(true);
this.sound(action);
this.peerActionProgress(from_peer_name, message, 5000, action);
}
break;
case 'mute':
if (peerActionAllowed) {
this.closeProducer(mediaType.audio);
this.updatePeerInfo(this.peer_name, this.peer_id, 'audio', false);
this.userLog(
'warning',
from_peer_name + ' ' + _PEER.audioOff + ' has closed yours audio',
'top-end',
10000,
);
}
break;
case 'unmute':
if (peerActionAllowed) {
this.peerMediaStartConfirm(
mediaType.audio,
image.unmute,
'Enable Microphone',
'Allow the presenter to enable your microphone?',
);
}
break;
case 'hide':
if (peerActionAllowed) {
this.closeProducer(mediaType.video);
this.userLog(
'warning',
from_peer_name + ' ' + _PEER.videoOff + ' has closed yours video',
'top-end',
10000,
);
}
break;
case 'unhide':
if (peerActionAllowed) {
this.peerMediaStartConfirm(
mediaType.video,
image.unhide,
'Enable Camera',
'Allow the presenter to enable your camera?',
);
}
break;
case 'stop':
if (this.isScreenShareSupported) {
if (peerActionAllowed) {
this.closeProducer(mediaType.screen);
this.userLog(
'warning',
from_peer_name + ' ' + _PEER.screenOff + ' has closed yours screen share',
'top-end',
10000,
);
}
}
break;
case 'start':
if (peerActionAllowed) {
this.peerMediaStartConfirm(
mediaType.screen,
image.start,
'Start Screen share',
'Allow the presenter to start your screen share?',
);
}
break;
default:
break;
//...
}
}
}
peerMediaStartConfirm(type, imageUrl, title, text) {
sound('notify');
Swal.fire({
background: swalBackground,
position: 'center',
imageUrl: imageUrl,
title: title,
text: text,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
switch (type) {
case mediaType.audio:
this.produce(mediaType.audio, microphoneSelect.value);
this.updatePeerInfo(this.peer_name, this.peer_id, 'audio', true);
break;
case mediaType.video:
this.produce(mediaType.video, videoSelect.value);
break;
case mediaType.screen:
this.produce(mediaType.screen);
break;
default:
break;
}
}
});
}
peerActionProgress(tt, msg, time, action = 'na') {
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
icon: action == 'eject' ? 'warning' : 'success',
title: tt,
html: msg,
timer: time,
timerProgressBar: true,
didOpen: () => {
Swal.showLoading();
},
}).then(() => {
switch (action) {
case 'refresh':
getRoomParticipants();
break;
case 'ban':
case 'eject':
this.exit();
break;
default:
break;
}
});
}
confirmPeerAction(action, data) {
console.log('Confirm peer action', action);
switch (action) {
case 'ban':
let banConfirmed = false;
Swal.fire({
background: swalBackground,
position: 'center',
imageUrl: image.forbidden,
title: 'Ban current participant',
input: 'text',
inputPlaceholder: 'Ban reason',
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
})
.then((result) => {
if (result.isConfirmed) {
banConfirmed = true;
const message = result.value;
if (message) data.message = message;
this.socket.emit('peerAction', data);
let peer = this.getId(data.peer_id);
if (peer) {
peer.parentNode.removeChild(peer);
participantsCount--;
refreshParticipantsCount(participantsCount);
}
}
})
.then(() => {
if (banConfirmed) this.peerActionProgress(action, 'In progress, wait...', 6000, 'refresh');
});
break;
case 'eject':
let ejectConfirmed = false;
let whoEject = data.broadcast ? 'All participants except yourself?' : 'current participant?';
Swal.fire({
background: swalBackground,
position: 'center',
imageUrl: data.broadcast ? image.users : image.user,
title: 'Eject ' + whoEject,
input: 'text',
inputPlaceholder: 'Eject reason',
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
})
.then((result) => {
if (result.isConfirmed) {
ejectConfirmed = true;
const message = result.value;
if (message) data.message = message;
if (!data.broadcast) {
this.socket.emit('peerAction', data);
let peer = this.getId(data.peer_id);
if (peer) {
peer.parentNode.removeChild(peer);
participantsCount--;
refreshParticipantsCount(participantsCount);
}
} else {
this.socket.emit('peerAction', data);
let actionButton = this.getId(action + 'AllButton');
if (actionButton) actionButton.style.display = 'none';
participantsCount = 1;
refreshParticipantsCount(participantsCount);
}
}
})
.then(() => {
if (ejectConfirmed) this.peerActionProgress(action, 'In progress, wait...', 6000, 'refresh');
});
break;
case 'mute':
case 'unmute':
case 'hide':
case 'unhide':
case 'stop':
case 'start':
let muteHideStopConfirmed = false;
let who = data.broadcast ? 'everyone except yourself?' : 'current participant?';
let imageUrl, title, text;
switch (action) {
case 'mute':
imageUrl = image.mute;
title = 'Mute ' + who;
text =
'Once muted, only the presenter will be able to unmute participants, but participants can unmute themselves at any time';
break;
case 'unmute':
imageUrl = image.unmute;
title = 'Unmute ' + who;
text = 'A pop-up message will appear to prompt and allow this action.';
break;
case 'hide':
title = 'Hide ' + who;
imageUrl = image.hide;
text =
'Once hidden, only the presenter will be able to unhide participants, but participants can unhide themselves at any time';
break;
case 'unhide':
title = 'Unhide ' + who;
imageUrl = image.unhide;
text = 'A pop-up message will appear to prompt and allow this action.';
break;
case 'stop':
imageUrl = image.stop;
title = 'Stop screen share to the ' + who;
text =
"Once stopped, only the presenter will be able to start the participants' screens, but participants can start their screens themselves at any time";
break;
case 'start':
imageUrl = image.start;
title = 'Start screen share to the ' + who;
text = 'A pop-up message will appear to prompt and allow this action.';
break;
default:
break;
}
Swal.fire({
background: swalBackground,
position: 'center',
imageUrl: imageUrl,
title: title,
text: text,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
})
.then((result) => {
if (result.isConfirmed) {
muteHideStopConfirmed = true;
if (!data.broadcast) {
switch (action) {
case 'mute':
let peerAudioButton = this.getId(data.peer_id + '___pAudio');
if (peerAudioButton) peerAudioButton.innerHTML = _PEER.audioOff;
break;
case 'hide':
let peerVideoButton = this.getId(data.peer_id + '___pVideo');
if (peerVideoButton) peerVideoButton.innerHTML = _PEER.videoOff;
case 'stop':
let peerScreenButton = this.getId(data.peer_id + '___pScreen');
if (peerScreenButton) peerScreenButton.innerHTML = _PEER.screenOff;
default:
break;
}
this.socket.emit('peerAction', data);
} else {
this.socket.emit('peerAction', data);
let actionButton = this.getId(action + 'AllButton');
if (actionButton) actionButton.style.display = 'none';
}
}
})
.then(() => {
if (muteHideStopConfirmed)
this.peerActionProgress(action, 'In progress, wait...', 2000, 'refresh');
});
break;
default:
break;
//...
}
}
peerGuestNotAllowed(action) {
console.log('peerGuestNotAllowed', action);
switch (action) {
case 'audio':
this.userLog('warning', 'Only the presenter can mute/unmute participants', 'top-end');
break;
case 'video':
this.userLog('warning', 'Only the presenter can hide/show participants', 'top-end');
break;
case 'screen':
this.userLog('warning', 'Only the presenter can start/stop the screen of participants', 'top-end');
break;
default:
break;
}
}
// ####################################################
// SEARCH PEER FILTER
// ####################################################
searchPeer() {
const searchParticipantsFromList = this.getId('searchParticipantsFromList');
const searchFilter = searchParticipantsFromList.value.toUpperCase();
const participantsList = this.getId('participantsList');
const participantsListItems = participantsList.getElementsByTagName('li');
for (let i = 0; i < participantsListItems.length; i++) {
const li = participantsListItems[i];
const participantName = li.getAttribute('data-to-name').toUpperCase();
const shouldDisplay = participantName.includes(searchFilter);
li.style.display = shouldDisplay ? '' : 'none';
}
}
// ####################################################
// FILTER PEER WITH RAISE HAND
// ####################################################
toggleRaiseHands() {
const participantsList = this.getId('participantsList');
const participantsListItems = participantsList.getElementsByTagName('li');
for (let i = 0; i < participantsListItems.length; i++) {
const li = participantsListItems[i];
const hasPulsateClass = li.querySelector('i.pulsate') !== null;
const shouldDisplay = (hasPulsateClass && !this.isToggleRaiseHand) || this.isToggleRaiseHand;
li.style.display = shouldDisplay ? '' : 'none';
}
this.isToggleRaiseHand = !this.isToggleRaiseHand;
setColor(participantsRaiseHandBtn, this.isToggleRaiseHand ? 'lime' : 'white');
}
// ####################################################
// FILTER PEER WITH UNREAD MESSAGES
// ####################################################
toggleUnreadMsg() {
const participantsList = this.getId('participantsList');
const participantsListItems = participantsList.getElementsByTagName('li');
for (let i = 0; i < participantsListItems.length; i++) {
const li = participantsListItems[i];
const shouldDisplay =
(li.classList.contains('pulsate') && !this.isToggleUnreadMsg) || this.isToggleUnreadMsg;
li.style.display = shouldDisplay ? '' : 'none';
}
this.isToggleUnreadMsg = !this.isToggleUnreadMsg;
setColor(participantsUnreadMessagesBtn, this.isToggleUnreadMsg ? 'lime' : 'white');
}
// ####################################################
// SHOW PEER ABOUT AND MESSAGES
// ####################################################
showPeerAboutAndMessages(peer_id, peer_name, event = null) {
this.hidePeerMessages();
const chatAbout = this.getId('chatAbout');
const participant = this.getId(peer_id);
const participantsList = this.getId('participantsList');
const chatPrivateMessages = this.getId('chatPrivateMessages');
const messagePrivateListItems = chatPrivateMessages.getElementsByTagName('li');
const participantsListItems = participantsList.getElementsByTagName('li');
const avatarImg = getParticipantAvatar(peer_name);
const generateChatAboutHTML = (imgSrc, title, status = 'online', participants = '') => {
const isSensitiveChat = !['all', 'ChatGPT'].includes(peer_id) && title.length > 15;
const truncatedTitle = isSensitiveChat ? `${title.substring(0, 10)}*****` : title;
return `
<img class="all-participants-img"
style="border: var(--border); width: 43px; margin-right: 5px; cursor: pointer;"
id="chatShowParticipantsList"
src="${image.users}"
alt="participants"
onclick="rc.toggleShowParticipants()"
/>
<a data-toggle="modal" data-target="#view_info">
<img src="${imgSrc}" alt="avatar" />
</a>
<div class="chat-about">
<h6 class="mb-0">${truncatedTitle}</h6>
<span class="status">
<i class="fa fa-circle ${status}"></i> ${status} ${participants}
</span>
</div>
`;
};
// CURRENT SELECTED PEER
for (let i = 0; i < participantsListItems.length; i++) {
participantsListItems[i].classList.remove('active');
participantsListItems[i].classList.remove('pulsate'); // private new message to read
if (!['all', 'ChatGPT'].includes(peer_id)) {
// icon private new message to read
this.getId(`${peer_id}-unread-msg`).classList.add('hidden');
}
}
participant.classList.add('active');
isChatGPTOn = false;
console.log('Display messages', peer_id);
switch (peer_id) {
case 'ChatGPT':
if (this._moderator.chat_cant_chatgpt) {
return userLog('warning', 'The moderator does not allow you to chat with ChatGPT', 'top-end', 6000);
}
isChatGPTOn = true;
chatAbout.innerHTML = generateChatAboutHTML(image.chatgpt, 'ChatGPT');
this.getId('chatGPTMessages').style.display = 'block';
break;
case 'all':
chatAbout.innerHTML = generateChatAboutHTML(image.all, 'Public chat', 'online', participantsCount);
this.getId('chatPublicMessages').style.display = 'block';
break;
default:
if (this._moderator.chat_cant_privately) {
return userLog('warning', 'The moderator does not allow you to chat privately', 'top-end', 6000);
}
chatAbout.innerHTML = generateChatAboutHTML(avatarImg, peer_name);
chatPrivateMessages.style.display = 'block';
for (let i = 0; i < messagePrivateListItems.length; i++) {
const li = messagePrivateListItems[i];
const itemFromId = li.getAttribute('data-from-id');
const itemToId = li.getAttribute('data-to-id');
const shouldDisplay = itemFromId.includes(peer_id) || itemToId.includes(peer_id);
li.style.display = shouldDisplay ? '' : 'none';
}
break;
}
if (!this.isMobileDevice) setTippy('chatShowParticipantsList', 'Toggle participants list', 'bottom');
const clickedElement = event ? event.target : null;
if (!event || (clickedElement.tagName != 'BUTTON' && clickedElement.tagName != 'I')) {
if ((this.isMobileDevice || this.isChatPinned) && (!plist || !plist.classList.contains('hidden'))) {
this.toggleShowParticipants();
}
}
}
hidePeerMessages() {
elemDisplay('chatGPTMessages', false);
elemDisplay('chatPublicMessages', false);
elemDisplay('chatPrivateMessages', false);
}
// ####################################################
// UPDATE ROOM MODERATOR
// ####################################################
updateRoomModerator(data) {
if (!isRulesActive || isPresenter) {
const moderator = this.getModeratorData(data);
this.socket.emit('updateRoomModerator', moderator);
}
}
updateRoomModeratorALL(data) {
if (!isRulesActive || isPresenter) {
const moderator = this.getModeratorData(data);
this.socket.emit('updateRoomModeratorALL', moderator);
}
}
getModeratorData(data) {
return {
peer_name: this.peer_name,
peer_uuid: this.peer_uuid,
moderator: data,
};
}
handleUpdateRoomModerator(data) {
switch (data.type) {
case 'audio_cant_unmute':
this._moderator.audio_cant_unmute = data.status;
this._moderator.audio_cant_unmute ? hide(tabAudioDevicesBtn) : show(tabAudioDevicesBtn);
rc.roomMessage('audio_cant_unmute', data.status);
break;
case 'video_cant_unhide':
this._moderator.video_cant_unhide = data.status;
this._moderator.video_cant_unhide ? hide(tabVideoDevicesBtn) : show(tabVideoDevicesBtn);
rc.roomMessage('video_cant_unhide', data.status);
case 'screen_cant_share':
this._moderator.screen_cant_share = data.status;
rc.roomMessage('screen_cant_share', data.status);
break;
case 'chat_cant_privately':
this._moderator.chat_cant_privately = data.status;
rc.roomMessage('chat_cant_privately', data.status);
break;
case 'chat_cant_chatgpt':
this._moderator.chat_cant_chatgpt = data.status;
rc.roomMessage('chat_cant_chatgpt', data.status);
break;
default:
break;
}
}
handleUpdateRoomModeratorALL(data) {
this._moderator = data;
console.log('Update Room Moderator data all', this._moderator);
}
getModerator() {
console.log('Get Moderator', this._moderator);
return this._moderator;
}
// ####################################################
// UPDATE PEER INFO
// ####################################################
updatePeerInfo(peer_name, peer_id, type, status, emit = true, presenter = false) {
if (emit) {
switch (type) {
case 'audio':
this.setIsAudio(peer_id, status);
break;
case 'video':
this.setIsVideo(status);
break;
case 'screen':
this.setIsScreen(status);
break;
case 'hand':
this.peer_info.peer_hand = status;
let peer_hand = this.getPeerHandBtn(peer_id);
if (status) {
if (peer_hand) peer_hand.style.display = 'flex';
this.event(_EVENTS.raiseHand);
this.sound('raiseHand');
} else {
if (peer_hand) peer_hand.style.display = 'none';
this.event(_EVENTS.lowerHand);
}
break;
default:
break;
}
const data = {
room_id: this.room_id,
peer_name: peer_name,
peer_id: peer_id,
type: type,
status: status,
broadcast: true,
};
this.socket.emit('updatePeerInfo', data);
} else {
const canUpdateMediaStatus = !isBroadcastingEnabled || (isBroadcastingEnabled && presenter);
switch (type) {
case 'audio':
if (canUpdateMediaStatus) this.setPeerAudio(peer_id, status);
break;
case 'video':
break;
case 'screen':
break;
case 'hand':
let peer_hand = this.getPeerHandBtn(peer_id);
if (status) {
if (peer_hand) peer_hand.style.display = 'flex';
this.userLog(
'warning',
peer_name + ' ' + _PEER.raiseHand + ' has raised the hand',
'top-end',
10000,
);
this.sound('raiseHand');
} else {
if (peer_hand) peer_hand.style.display = 'none';
}
break;
default:
break;
}
}
if (isParticipantsListOpen) getRoomParticipants();
}
checkPeerInfoStatus(peer_info) {
let peer_id = peer_info.peer_id;
let peer_hand_status = peer_info.peer_hand;
if (peer_hand_status) {
let peer_hand = this.getPeerHandBtn(peer_id);
if (peer_hand) peer_hand.style.display = 'flex';
}
//...
}
popupPeerInfo(id, peer_info) {
if (this.showPeerInfo && !this.isMobileDevice) {
this.setTippy(
id,
'<pre>' +
JSON.stringify(
peer_info,
[
'join_data_time',
'peer_id',
'peer_name',
'peer_audio',
'peer_video',
'peer_video_privacy',
'peer_screen',
'peer_hand',
'is_desktop_device',
'is_mobile_device',
'is_tablet_device',
'is_ipad_pro_device',
'os_name',
'os_version',
'browser_name',
'browser_version',
//'user_agent',
],
2,
) +
'<pre/>',
'top-start',
true,
);
}
}
// ####################################################
// HANDLE PEER GEOLOCATION
// ####################################################
askPeerGeoLocation(id) {
const words = id.split('___');
const peer_id = words[0];
const cmd = {
type: 'geoLocation',
from_peer_name: this.peer_name,
from_peer_id: this.peer_id,
peer_id: peer_id,
broadcast: false,
};
this.emitCmd(cmd);
this.peerActionProgress(
'Geolocation',
'Geolocation requested. Please wait for confirmation...',
6000,
'geolocation',
);
}
sendPeerGeoLocation(peer_id, type, data) {
const cmd = {
type: type,
from_peer_name: this.peer_name,
from_peer_id: this.peer_id,
peer_id: peer_id,
data: data,
broadcast: false,
};
this.emitCmd(cmd);
}
confirmPeerGeoLocation(cmd) {
this.sound('notify');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
imageUrl: image.geolocation,
position: 'center',
title: 'Geo Location',
html: `Would you like to share your location to ${cmd.from_peer_name}?`,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
result.isConfirmed ? this.getPeerGeoLocation(cmd.from_peer_id) : this.denyPeerGeoLocation(cmd.from_peer_id);
});
}
getPeerGeoLocation(peer_id) {
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
function (position) {
const geoLocation = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
};
console.log('GeoLocation --->', geoLocation);
rc.sendPeerGeoLocation(peer_id, 'geoLocationOK', geoLocation);
// openURL(`https://www.openstreetmap.org/?mlat=${geoLocation.latitude}&mlon=${geoLocation.longitude}`, true);
// openURL(`http://maps.apple.com/?ll=${geoLocation.latitude},${geoLocation.longitude}`, true);
// openURL(`https://www.google.com/maps/search/?api=1&query=${geoLocation.latitude},${geoLocation.longitude}`, true);
},
function (error) {
let geoError = error;
switch (error.code) {
case error.PERMISSION_DENIED:
geoError = 'User denied the request for Geolocation';
break;
case error.POSITION_UNAVAILABLE:
geoError = 'Location information is unavailable';
break;
case error.TIMEOUT:
geoError = 'The request to get user location timed out';
break;
case error.UNKNOWN_ERROR:
geoError = 'An unknown error occurred';
break;
}
rc.sendPeerGeoLocation(peer_id, 'geoLocationKO', `${rc.peer_name}: ${geoError}`);
rc.userLog('warning', geoError, 'top-end', 5000);
},
);
} else {
rc.sendPeerGeoLocation(
peer_id,
'geoLocationKO',
`${rc.peer_name}: Geolocation is not supported by this browser`,
);
rc.userLog('warning', 'Geolocation is not supported by this browser', 'top-end', 5000);
}
}
denyPeerGeoLocation(peer_id) {
rc.sendPeerGeoLocation(peer_id, 'geoLocationKO', `${rc.peer_name}: Has declined permission for geolocation`);
}
handleGeoPeerLocation(cmd) {
const geoLocation = cmd.data;
this.sound('notify');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
imageUrl: image.geolocation,
position: 'center',
title: 'Geo Location',
html: `Would you like to open ${cmd.from_peer_name} geolocation?`,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
// openURL(`https://www.openstreetmap.org/?mlat=${geoLocation.latitude}&mlon=${geoLocation.longitude}`, true);
// openURL(`http://maps.apple.com/?ll=${geoLocation.latitude},${geoLocation.longitude}`, true);
openURL(
`https://www.google.com/maps/search/?api=1&query=${geoLocation.latitude},${geoLocation.longitude}`,
true,
);
}
});
}
} // End