'use strict';
if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.href.substr(4, location.href.length - 4);
/**
* MiroTalk SFU - Room component
*
* @link GitHub: https://github.com/miroslavpejic85/mirotalksfu
* @link Official Live demo: https://sfu.mirotalk.com
* @license For open source use: AGPLv3
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.9.46
*
*/
// ####################################################
// STATIC SETTINGS
// ####################################################
console.log('Window Location', window.location);
const userAgent = navigator.userAgent;
const parser = new UAParser(userAgent);
const parserResult = parser.getResult();
const deviceType = parserResult.device.type || 'desktop';
const isMobileDevice = deviceType === 'mobile';
const isTabletDevice = deviceType === 'tablet';
const isIPadDevice = parserResult.device.model?.toLowerCase() === 'ipad';
const isDesktopDevice = deviceType === 'desktop';
const isFirefox = parserResult.browser.name?.toLowerCase() === 'firefox';
const thisInfo = getInfo();
const isEmbedded = window.self !== window.top;
const showDocumentPipBtn = !isEmbedded && 'documentPictureInPicture' in window;
/**
* Initializes a Socket.IO client instance with custom connection and reconnection options.
*
* @property {string[]} transports - The transport mechanisms to use. Default: ['polling', 'websocket']. Here, only ['websocket'] is used.
* @property {boolean} reconnection - Whether to automatically reconnect if connection is lost. Default: true.
* @property {number} reconnectionAttempts - Maximum number of reconnection attempts before giving up. Default: Infinity. Here, set to 10.
* @property {number} reconnectionDelay - How long to initially wait before attempting a new reconnection (in ms). Default: 1000. Here, set to 3000.
* @property {number} reconnectionDelayMax - Maximum amount of time to wait between reconnections (in ms). Default: 5000. Here, set to 15000.
* @property {number} timeout - Connection timeout before an error is emitted (in ms). Default: 20000.
*/
const socket = io({
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 3000,
reconnectionDelayMax: 15000,
timeout: 20000,
});
let survey = {
enabled: true,
url: 'https://www.questionpro.com/t/AUs7VZq02P',
};
let redirect = {
enabled: true,
url: '/newroom',
};
let recCodecs = null;
let isToggleExtraBtnClicked = false;
const _PEER = {
presenter: '',
guest: '',
audioOn: '',
audioOff: '',
videoOn: '',
videoOff: '',
screenOn: '',
screenOff: '',
raiseHand: '',
lowerHand: '',
acceptPeer: '',
banPeer: '',
ejectPeer: '',
geoLocation: '',
sendFile: '',
sendMsg: '',
sendVideo: '',
};
const initUser = document.getElementById('initUser');
const initVideoContainerClass = document.querySelector('.init-video-container');
const bars = document.querySelectorAll('.volume-bar');
const Base64Prefix = 'data:application/pdf;base64,';
// Whiteboard
const wbImageInput = 'image/*';
const wbPdfInput = 'application/pdf';
const wbWidth = 1366;
const wbHeight = 768;
const wbGridSize = 20;
const wbStroke = '#cccccc63';
let wbGridLines = [];
let wbGridVisible = false;
const swalImageUrl = '../images/pricing-illustration.svg';
// Media
const sinkId = 'sinkId' in HTMLMediaElement.prototype;
// ####################################################
// LOCAL STORAGE
// ####################################################
const lS = new LocalStorage();
const localStorageSettings = lS.getLocalStorageSettings() || lS.SFU_SETTINGS;
const localStorageDevices = lS.getLocalStorageDevices() || lS.LOCAL_STORAGE_DEVICES;
const localStorageInitConfig = lS.getLocalStorageInitConfig() || lS.INIT_CONFIG;
console.log('LOCAL_STORAGE', {
localStorageSettings: localStorageSettings,
localStorageDevices: localStorageDevices,
localStorageInitConfig: localStorageInitConfig,
});
// ####################################################
// THEME CUSTOM COLOR - PICKER
// ####################################################
const themeCustom = {
input: document.getElementById('themeColorPicker'),
color: localStorageSettings.theme_color ? localStorageSettings.theme_color : '#000000',
keep: localStorageSettings.theme_custom ? localStorageSettings.theme_custom : false,
};
const pickr = Pickr.create({
el: themeCustom.input,
theme: 'classic', // or 'monolith', or 'nano'
default: themeCustom.color,
useAsButton: true,
swatches: [
'rgba(244, 67, 54, 1)',
'rgba(233, 30, 99, 0.95)',
'rgba(156, 39, 176, 0.9)',
'rgba(103, 58, 183, 0.85)',
'rgba(63, 81, 181, 0.8)',
'rgba(33, 150, 243, 0.75)',
'rgba(3, 169, 244, 0.7)',
'rgba(0, 188, 212, 0.7)',
'rgba(0, 150, 136, 0.75)',
'rgba(76, 175, 80, 0.8)',
'rgba(139, 195, 74, 0.85)',
'rgba(205, 220, 57, 0.9)',
'rgba(255, 235, 59, 0.95)',
'rgba(255, 193, 7, 1)',
],
components: {
// Main components
preview: true,
opacity: true,
hue: true,
// Input / output Options
interaction: {
hex: false,
rgba: false,
hsla: false,
hsva: false,
cmyk: false,
input: false,
clear: false,
save: false,
},
},
})
.on('init', (pickr) => {
themeCustom.input.value = pickr.getSelectedColor().toHEXA().toString(0);
})
.on('change', (color) => {
themeCustom.color = color.toHEXA().toString();
themeCustom.input.value = themeCustom.color;
setCustomTheme();
})
.on('changestop', (color) => {
localStorageSettings.theme_color = themeCustom.color;
lS.setSettings(localStorageSettings);
});
// ####################################################
// ENUMERATE DEVICES SELECTS
// ####################################################
const videoSelect = getId('videoSelect');
const videoQuality = getId('videoQuality');
const videoFps = getId('videoFps');
const screenFps = getId('screenFps');
const screenOptimization = getId('screenOptimization');
const initVideoSelect = getId('initVideoSelect');
const microphoneSelect = getId('microphoneSelect');
const initMicrophoneSelect = getId('initMicrophoneSelect');
const speakerSelect = getId('speakerSelect');
const initSpeakerSelect = getId('initSpeakerSelect');
// ####################################################
// VIRTUAL BACKGROUND DEFAULT IMAGES AND INIT CLASS
// ####################################################
const virtualBackgrounds = Object.values(image.virtualBackground);
const virtualBackground = new VirtualBackground();
const isMediaStreamTrackAndTransformerSupported = virtualBackground.checkSupport();
// ####################################################
// DYNAMIC SETTINGS
// ####################################################
let preventExit = false;
let virtualBackgroundBlurLevel;
let virtualBackgroundSelectedImage;
let virtualBackgroundTransparent;
let swalBackground = 'radial-gradient(#393939, #000000)'; //'rgba(0, 0, 0, 0.7)';
let rc = null;
let producer = null;
let participantsCount = 0;
let lobbyParticipantsCount = 0;
let chatMessagesId = 0;
let room_id = getRoomId();
let room_password = getRoomPassword();
let room_duration = getRoomDuration();
let peer_name = getPeerName();
let peer_avatar = getPeerAvatar();
let peer_uuid = getPeerUUID();
let peer_token = getPeerToken();
let isScreenAllowed = getScreen();
let isHideMeActive = getHideMeActive();
let notify = getNotify();
let chat = getChat();
isPresenter = isPeerPresenter();
let peer_info = null;
let isPushToTalkActive = false;
let isSpaceDown = false;
let isPitchBarEnabled = true;
let isSoundEnabled = true;
let isKeepButtonsVisible = false;
let isShortcutsEnabled = false;
let isBroadcastingEnabled = false;
let isLobbyEnabled = false;
let isLobbyOpen = false;
let hostOnlyRecording = false;
let isEnumerateAudioDevices = false;
let isEnumerateVideoDevices = false;
let isAudioAllowed = false;
let isVideoAllowed = false;
let isVideoPrivacyActive = false;
let isRecording = false;
let isAudioVideoAllowed = false;
let isParticipantsListOpen = false;
let isVideoControlsOn = false;
let isChatPasteTxt = false;
let isChatMarkdownOn = false;
let isChatGPTOn = false;
let isDeepSeekOn = false;
let isSpeechSynthesisSupported = 'speechSynthesis' in window;
let joinRoomWithoutAudioVideo = true;
let joinRoomWithScreen = false;
let audio = false;
let video = false;
let screen = false;
let hand = false;
let camera = 'user';
let recTimer = null;
let recElapsedTime = null;
let wbCanvas = null;
let wbIsLock = false;
let wbIsDrawing = false;
let wbIsOpen = false;
let wbIsRedoing = false;
let wbIsEraser = false;
let wbIsBgTransparent = false;
let wbPop = [];
let coords = {};
let isButtonsVisible = false;
let isButtonsBarOver = false;
let isRoomLocked = false;
let initStream = null;
let audioContext = null;
let workletNode = null;
// window.location.origin + '/join/' + roomId
// window.location.origin + '/join/?room=' + roomId + '&token=' + myToken
let RoomURL = window.location.origin + '/join/' + room_id;
let isExiting = false;
let transcription;
let showFreeAvatars = true;
let quill = null;
// ####################################################
// INIT ROOM
// ####################################################
document.addEventListener('DOMContentLoaded', function () {
initClient();
});
function initClient() {
setTheme();
// Transcription
transcription = new Transcription();
transcription.init();
if (!isMobileDevice) {
refreshMainButtonsToolTipPlacement();
setTippy('closeEmojiPickerContainer', 'Close', 'bottom');
setTippy('mySettingsCloseBtn', 'Close', 'bottom');
setTippy(
'switchDominantSpeakerFocus',
'If Active, When a participant speaks, their video will be focused and enlarged',
'right'
);
setTippy(
'switchNoiseSuppression',
'If Active, the audio will be processed to reduce background noise, making the voice clearer',
'right'
);
setTippy(
'switchPushToTalk',
'If Active, When SpaceBar keydown the microphone will be resumed, on keyup will be paused, like a walkie-talkie',
'right'
);
setTippy('lobbyAcceptAllBtn', 'Accept', 'top');
setTippy('lobbyRejectAllBtn', 'Reject', 'top');
setTippy(
'switchBroadcasting',
'Broadcasting is the dissemination of audio or video content to a large audience (one to many)',
'right'
);
setTippy(
'switchLobby',
'Lobby mode lets you protect your meeting by only allowing people to enter after a formal approval by a moderator',
'right'
);
setTippy('initVideoAudioRefreshButton', 'Refresh audio/video devices', 'top');
setTippy(
'screenOptimizationLabel',
'Detail: For high fidelity (screen sharing with text/graphics)
Motion: For high frame rate (video playback, game streaming',
'right',
true
);
setTippy('switchPitchBar', 'Toggle audio pitch bar', 'right');
setTippy('switchSounds', 'Toggle the sounds notifications', 'right');
setTippy('switchShare', "Show 'Share Room' popup on join", 'right');
setTippy('switchKeepButtonsVisible', 'Keep buttons always visible', 'right');
setTippy('roomId', 'Room name (click to copy)', 'right');
setTippy('sessionTime', 'Session time', 'right');
setTippy('recordingImage', 'Toggle recording', 'right');
setTippy(
'switchHostOnlyRecording',
'Only the host (presenter) has the capability to record the meeting',
'right'
);
setTippy(
'switchH264Recording',
'Prioritize h.264 with AAC or h.264 with Opus codecs over VP8 with Opus or VP9 with Opus codecs',
'right'
);
setTippy('refreshVideoFiles', 'Refresh', 'left');
setTippy('switchServerRecording', 'The recording will be stored on the server rather than locally', 'right');
setTippy('whiteboardGhostButton', 'Toggle transparent background', 'bottom');
setTippy('whiteboardGridBtn', 'Toggle whiteboard grid', 'bottom');
setTippy('wbBackgroundColorEl', 'Background color', 'bottom');
setTippy('wbDrawingColorEl', 'Drawing color', 'bottom');
setTippy('whiteboardPencilBtn', 'Drawing mode', 'bottom');
setTippy('whiteboardObjectBtn', 'Object mode', 'bottom');
setTippy('whiteboardUndoBtn', 'Undo', 'bottom');
setTippy('whiteboardRedoBtn', 'Redo', 'bottom');
setTippy('whiteboardLockBtn', 'Toggle Lock whiteboard', 'right');
setTippy('whiteboardUnlockBtn', 'Toggle Lock whiteboard', 'right');
setTippy('whiteboardCloseBtn', 'Close', 'right');
setTippy('chatCleanTextButton', 'Clean', 'top');
setTippy('chatPasteButton', 'Paste', 'top');
setTippy('chatSendButton', 'Send', 'top');
setTippy('showChatOnMsg', 'Show chat on new message comes', 'bottom');
setTippy('speechIncomingMsg', 'Speech the incoming messages', 'bottom');
setTippy('chatSpeechStartButton', 'Start speech recognition', 'top');
setTippy('chatSpeechStopButton', 'Stop speech recognition', 'top');
setTippy('chatEmojiButton', 'Emoji', 'top');
setTippy('chatMarkdownButton', 'Markdown', 'top');
setTippy('chatCloseButton', 'Close', 'bottom');
setTippy('chatTogglePin', 'Toggle pin', 'bottom');
setTippy('chatHideParticipantsList', 'Hide', 'bottom');
setTippy('chatShowParticipantsList', 'Toggle participants list', 'bottom');
setTippy('chatMaxButton', 'Maximize', 'bottom');
setTippy('chatMinButton', 'Minimize', 'bottom');
setTippy('pollTogglePin', 'Toggle pin', 'bottom');
setTippy('pollMaxButton', 'Maximize', 'bottom');
setTippy('pollMinButton', 'Minimize', 'bottom');
setTippy('pollSaveButton', 'Save results', 'bottom');
setTippy('pollCloseBtn', 'Close', 'bottom');
setTippy('editorLockBtn', 'Toggle Lock editor', 'bottom');
setTippy('editorUnlockBtn', 'Toggle Lock editor', 'bottom');
setTippy('editorTogglePin', 'Toggle pin', 'bottom');
setTippy('editorUndoBtn', 'Undo', 'bottom');
setTippy('editorRedoBtn', 'Redo', 'bottom');
setTippy('editorCopyBtn', 'Copy', 'bottom');
setTippy('editorSaveBtn', 'Save', 'bottom');
setTippy('editorCloseBtn', 'Close', 'bottom');
setTippy('editorCleanBtn', 'Clean', 'bottom');
setTippy('pollAddOptionBtn', 'Add option', 'top');
setTippy('pollDelOptionBtn', 'Delete option', 'top');
setTippy('participantsSaveBtn', 'Save participants info', 'bottom');
setTippy('participantsRaiseHandBtn', 'Toggle raise hands', 'bottom');
setTippy('participantsUnreadMessagesBtn', 'Toggle unread messages', 'bottom');
setTippy('transcriptionCloseBtn', 'Close', 'bottom');
setTippy('transcriptionTogglePinBtn', 'Toggle pin', 'bottom');
setTippy('transcriptionMaxBtn', 'Maximize', 'bottom');
setTippy('transcriptionMinBtn', 'Minimize', 'bottom');
setTippy('transcriptionSpeechStatus', 'Status', 'bottom');
setTippy('transcriptShowOnMsg', 'Show transcript on new message comes', 'bottom');
setTippy('transcriptionSpeechStart', 'Start transcription', 'top');
setTippy('transcriptionSpeechStop', 'Stop transcription', 'top');
}
setupWhiteboard();
initEnumerateDevices();
setupInitButtons();
}
// ####################################################
// HANDLE MAIN BUTTONS TOOLTIP
// ####################################################
function refreshMainButtonsToolTipPlacement() {
if (!isMobileDevice) {
//
const position = BtnsBarPosition.options[BtnsBarPosition.selectedIndex].value;
const placement = position == 'vertical' ? 'right' : 'top';
const bPlacement = position == 'vertical' ? 'top' : 'right';
// Control buttons
setTippy('shareButton', 'Share room', placement);
setTippy('hideMeButton', 'Toggle hide self view', placement);
setTippy('startRecButton', 'Start recording', placement);
setTippy('stopRecButton', 'Stop recording', placement);
setTippy('fullScreenButton', 'Toggle full screen', placement);
setTippy('emojiRoomButton', 'Toggle emoji reaction', placement);
setTippy('pollButton', 'Toggle the poll', placement);
setTippy('editorButton', 'Toggle the editor', placement);
setTippy('transcriptionButton', 'Toggle transcription', placement);
setTippy('whiteboardButton', 'Toggle the whiteboard', placement);
setTippy('documentPiPButton', 'Toggle Document picture in picture', placement);
setTippy('snapshotRoomButton', 'Snapshot screen, window, or tab', placement);
setTippy('restartICEButton', 'Restart ICE', placement);
setTippy('aboutButton', 'About this project', placement);
// Bottom buttons
setTippy('toggleExtraButton', 'Toggle extra buttons', bPlacement);
setTippy('startAudioButton', 'Start the audio', bPlacement);
setTippy('stopAudioButton', 'Stop the audio', bPlacement);
setTippy('startVideoButton', 'Start the video', bPlacement);
setTippy('stopVideoButton', 'Stop the video', bPlacement);
setTippy('swapCameraButton', 'Swap the camera', bPlacement);
setTippy('startScreenButton', 'Start screen share', bPlacement);
setTippy('stopScreenButton', 'Stop screen share', bPlacement);
setTippy('raiseHandButton', 'Raise your hand', bPlacement);
setTippy('lowerHandButton', 'Lower your hand', bPlacement);
setTippy('chatButton', 'Toggle the chat', bPlacement);
setTippy('settingsButton', 'Toggle the settings', bPlacement);
setTippy('exitButton', 'Leave room', bPlacement);
}
}
// ####################################################
// HANDLE TOOLTIP
// ####################################################
function setTippy(elem, content, placement, allowHTML = false) {
const element = document.getElementById(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);
}
}
// ####################################################
// HELPERS
// ####################################################
function getQueryParam(param) {
const urlParams = new URLSearchParams(window.location.search);
return filterXSS(urlParams.get(param));
}
// ####################################################
// GET ROOM ID
// ####################################################
function getRoomId() {
let queryRoomId = getQueryParam('room');
let roomId = queryRoomId ? queryRoomId : location.pathname.substring(6);
if (roomId == '') {
roomId = makeId(12);
}
console.log('Direct join', { room: roomId });
window.localStorage.lastRoom = roomId;
return roomId;
}
function makeId(length) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
// ####################################################
// INIT ROOM
// ####################################################
async function initRoom() {
if (!isAudioAllowed && !isVideoAllowed && !joinRoomWithoutAudioVideo) {
openURL(`/permission?room_id=${room_id}&message=Not allowed both Audio and Video`);
} else {
setButtonsInit();
handleSelectsInit();
handleUsernameEmojiPicker();
await whoAreYou();
await setSelectsInit();
}
}
// ####################################################
// ENUMERATE DEVICES
// ####################################################
async function initEnumerateDevices() {
console.log('01 ----> init Enumerate Devices');
await initEnumerateVideoDevices();
await initEnumerateAudioDevices();
await initRoom();
}
async function refreshMyAudioVideoDevices() {
await refreshMyVideoDevices();
await refreshMyAudioDevices();
}
async function refreshMyVideoDevices() {
if (!isVideoAllowed) return;
const initVideoSelectIndex = initVideoSelect ? initVideoSelect.selectedIndex : 0;
const videoSelectIndex = videoSelect ? videoSelect.selectedIndex : 0;
await initEnumerateVideoDevices();
if (initVideoSelect) initVideoSelect.selectedIndex = initVideoSelectIndex;
if (videoSelect) videoSelect.selectedIndex = videoSelectIndex;
}
async function refreshMyAudioDevices() {
if (!isAudioAllowed) return;
const initMicrophoneSelectIndex = initMicrophoneSelect ? initMicrophoneSelect.selectedIndex : 0;
const initSpeakerSelectIndex = initSpeakerSelect ? initSpeakerSelect.selectedIndex : 0;
const microphoneSelectIndex = microphoneSelect ? microphoneSelect.selectedIndex : 0;
const speakerSelectIndex = speakerSelect ? speakerSelect.selectedIndex : 0;
await initEnumerateAudioDevices();
if (initMicrophoneSelect) initMicrophoneSelect.selectedIndex = initMicrophoneSelectIndex;
if (initSpeakerSelect) initSpeakerSelect.selectedIndex = initSpeakerSelectIndex;
if (microphoneSelect) microphoneSelect.selectedIndex = microphoneSelectIndex;
if (speakerSelect) speakerSelect.selectedIndex = speakerSelectIndex;
}
async function initEnumerateVideoDevices() {
// allow the video
await navigator.mediaDevices
.getUserMedia({ video: true })
.then(async (stream) => {
await enumerateVideoDevices(stream);
isVideoAllowed = true;
})
.catch(() => {
isVideoAllowed = false;
});
}
async function enumerateVideoDevices(stream) {
console.log('02 ----> Get Video Devices');
if (videoSelect) videoSelect.innerHTML = '';
if (initVideoSelect) initVideoSelect.innerHTML = '';
await navigator.mediaDevices
.enumerateDevices()
.then((devices) =>
devices.forEach(async (device) => {
let el,
eli = null;
if ('videoinput' === device.kind) {
if (videoSelect) el = videoSelect;
if (initVideoSelect) eli = initVideoSelect;
lS.DEVICES_COUNT.video++;
}
if (!el) return;
await addChild(device, [el, eli]);
})
)
.then(async () => {
await stopTracks(stream);
isEnumerateVideoDevices = true;
});
}
async function initEnumerateAudioDevices() {
// allow the audio
await navigator.mediaDevices
.getUserMedia({ audio: true })
.then(async (stream) => {
await enumerateAudioDevices(stream);
await getMicrophoneVolumeIndicator(stream);
isAudioAllowed = true;
})
.catch(() => {
isAudioAllowed = false;
});
}
async function enumerateAudioDevices(stream) {
console.log('03 ----> Get Audio Devices');
if (microphoneSelect) microphoneSelect.innerHTML = '';
if (initMicrophoneSelect) initMicrophoneSelect.innerHTML = '';
if (speakerSelect) speakerSelect.innerHTML = '';
if (initSpeakerSelect) initSpeakerSelect.innerHTML = '';
await navigator.mediaDevices
.enumerateDevices()
.then((devices) =>
devices.forEach(async (device) => {
let el,
eli = null;
if ('audioinput' === device.kind) {
if (microphoneSelect) el = microphoneSelect;
if (initMicrophoneSelect) eli = initMicrophoneSelect;
lS.DEVICES_COUNT.audio++;
} else if ('audiooutput' === device.kind) {
if (speakerSelect) el = speakerSelect;
if (initSpeakerSelect) eli = initSpeakerSelect;
lS.DEVICES_COUNT.speaker++;
}
if (!el) return;
await addChild(device, [el, eli]);
})
)
.then(async () => {
await stopTracks(stream);
isEnumerateAudioDevices = true;
speakerSelect.disabled = !sinkId;
// Check if there is speakers
if (!sinkId || initSpeakerSelect.options.length === 0) {
hide(initSpeakerSelect);
hide(speakerSelectDiv);
}
});
}
async function stopTracks(stream) {
stream.getTracks().forEach((track) => {
track.stop();
});
}
async function addChild(device, els) {
let kind = device.kind;
els.forEach((el) => {
let option = document.createElement('option');
option.value = device.deviceId;
switch (kind) {
case 'videoinput':
option.innerText = `📹 ` + device.label || `📹 camera ${el.length + 1}`;
break;
case 'audioinput':
option.innerText = `🎤 ` + device.label || `🎤 microphone ${el.length + 1}`;
break;
case 'audiooutput':
option.innerText = `🔈 ` + device.label || `🔈 speaker ${el.length + 1}`;
break;
default:
break;
}
el.appendChild(option);
});
}
// ####################################################
// INIT AUDIO/VIDEO/SCREEN BUTTONS
// ####################################################
function setupInitButtons() {
initVideoAudioRefreshButton.onclick = () => {
refreshMyAudioVideoDevices();
};
initVideoButton.onclick = () => {
handleVideo();
};
initAudioButton.onclick = () => {
handleAudio();
};
initAudioVideoButton.onclick = async (e) => {
await handleAudioVideo(e);
};
initStartScreenButton.onclick = async () => {
await toggleScreenSharing();
};
initStopScreenButton.onclick = async () => {
await toggleScreenSharing();
};
initVideoMirrorButton.onclick = () => {
initVideo.classList.toggle('mirror');
};
initVirtualBackgroundButton.onclick = () => {
showImageSelector();
};
initUsernameEmojiButton.onclick = () => {
getId('usernameInput').value = '';
toggleUsernameEmoji();
};
}
// ####################################################
// MICROPHONE VOLUME INDICATOR
// ####################################################
async function getMicrophoneVolumeIndicator(stream) {
if (isAudioContextSupported() && hasAudioTrack(stream)) {
try {
stopMicrophoneProcessing();
console.log('Start microphone volume indicator for audio track', stream.getAudioTracks()[0]);
audioContext = new (window.AudioContext || window.webkitAudioContext)();
const microphone = audioContext.createMediaStreamSource(stream);
await audioContext.audioWorklet.addModule('/js/VolumeProcessor.js');
workletNode = new AudioWorkletNode(audioContext, 'volume-processor');
// Handle data from VolumeProcessor.js
workletNode.port.onmessage = (event) => {
const data = event.data;
switch (data.type) {
case 'volumeIndicator':
updateVolumeIndicator(data.volume);
break;
//...
default:
console.warn('Unknown message type from VolumeProcessor:', data.type);
break;
}
};
microphone.connect(workletNode);
workletNode.connect(audioContext.destination);
} catch (error) {
console.error('Error initializing microphone volume indicator:', error);
stopMicrophoneProcessing();
}
} else {
console.warn('Microphone volume indicator not supported for this browser');
}
}
function stopMicrophoneProcessing() {
console.log('Stop microphone volume indicator');
if (workletNode) {
try {
workletNode.disconnect();
} catch (error) {
console.warn('Error disconnecting workletNode:', error);
}
workletNode = null;
}
if (audioContext) {
try {
if (audioContext.state !== 'closed') {
audioContext.close();
}
} catch (error) {
console.warn('Error closing audioContext:', error);
}
audioContext = null;
}
}
function updateVolumeIndicator(volume) {
const normalizedVolume = Math.max(0, Math.min(1, volume));
const activeBars = Math.round(normalizedVolume * bars.length);
bars.forEach((bar, index) => {
bar.classList.toggle('active', index < activeBars);
});
}
function isAudioContextSupported() {
return !!(window.AudioContext || window.webkitAudioContext);
}
function hasAudioTrack(mediaStream) {
if (!mediaStream) return false;
const audioTracks = mediaStream.getAudioTracks();
return audioTracks.length > 0;
}
function hasVideoTrack(mediaStream) {
if (!mediaStream) return false;
const videoTracks = mediaStream.getVideoTracks();
return videoTracks.length > 0;
}
// ####################################################
// QUERY PARAMS CHECK
// ####################################################
function getScreen() {
let screen = getQueryParam('screen');
if (screen) {
screen = screen.toLowerCase();
let queryScreen = screen === '1' || screen === 'true';
if (queryScreen != null && (navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia)) {
console.log('Direct join', { screen: queryScreen });
return queryScreen;
}
}
console.log('Direct join', { screen: false });
return false;
}
function getNotify() {
let notify = getQueryParam('notify');
if (notify) {
notify = notify.toLowerCase();
let queryNotify = notify === '1' || notify === 'true';
if (queryNotify != null) {
console.log('Direct join', { notify: queryNotify });
return queryNotify;
}
}
notify = localStorageSettings.share_on_join;
console.log('Direct join', { notify: notify });
return notify;
}
function getChat() {
let chat = getQueryParam('chat');
if (chat) {
chat = chat.toLowerCase();
let queryChat = chat === '1' || chat === 'true';
if (queryChat != null) {
console.log('Direct join', { chat: queryChat });
return queryChat;
}
}
console.log('Direct join', { chat: chat });
return chat;
}
function getHideMeActive() {
let hide = getQueryParam('hide');
let queryHideMe = false;
if (hide) {
hide = hide.toLowerCase();
queryHideMe = hide === '1' || hide === 'true';
}
console.log('Direct join', { hide: queryHideMe });
return queryHideMe;
}
function isPeerPresenter() {
let presenter = getQueryParam('isPresenter');
if (presenter) {
presenter = presenter.toLowerCase();
let queryPresenter = presenter === '1' || presenter === 'true';
if (queryPresenter != null) {
console.log('Direct join Reconnect', { isPresenter: queryPresenter });
return queryPresenter;
}
}
console.log('Direct join Reconnect', { presenter: false });
return false;
}
function getPeerName() {
const name = getQueryParam('name');
if (isHtml(name)) {
console.log('Direct join', { name: 'Invalid name' });
return 'Invalid name';
}
console.log('Direct join', { name: name });
return name;
}
function getPeerAvatar() {
const avatar = getQueryParam('avatar');
const avatarDisabled = avatar === '0' || avatar === 'false';
console.log('Direct join', { avatar: avatar });
if (avatarDisabled || !isImageURL(avatar)) {
return false;
}
return avatar;
}
function getPeerUUID() {
if (lS.getItemLocalStorage('peer_uuid')) {
return lS.getItemLocalStorage('peer_uuid');
}
const peer_uuid = getUUID();
lS.setItemLocalStorage('peer_uuid', peer_uuid);
return peer_uuid;
}
function getPeerToken() {
if (window.sessionStorage.peer_token) return window.sessionStorage.peer_token;
let token = getQueryParam('token');
let queryToken = false;
if (token) {
queryToken = token;
}
console.log('Direct join', { token: queryToken });
return queryToken;
}
function getRoomPassword() {
let roomPassword = getQueryParam('roomPassword');
if (roomPassword) {
let queryNoRoomPassword = roomPassword === '0' || roomPassword === 'false';
if (queryNoRoomPassword) {
roomPassword = false;
}
console.log('Direct join', { password: roomPassword });
return roomPassword;
}
return false;
}
function getRoomDuration() {
const roomDuration = getQueryParam('duration');
if (isValidDuration(roomDuration)) {
if (roomDuration === 'unlimited') {
console.log('The room has no time limit');
return roomDuration;
}
const timeLimit = timeToMilliseconds(roomDuration);
setTimeout(() => {
sound('eject');
Swal.fire({
background: swalBackground,
position: 'center',
title: 'Time Limit Reached',
text: 'The room has reached its time limit and will close shortly',
icon: 'warning',
timer: 6000, // 6 seconds
timerProgressBar: true,
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
},
willClose: () => {
rc.exitRoom(true);
},
});
}, timeLimit);
console.log('Direct join', { duration: roomDuration, timeLimit: timeLimit });
return roomDuration;
}
return 'unlimited';
}
function timeToMilliseconds(timeString) {
const [hours, minutes, seconds] = timeString.split(':').map(Number);
return (hours * 3600 + minutes * 60 + seconds) * 1000;
}
function isValidDuration(duration) {
if (duration === 'unlimited') return true;
// Check if the format is HH:MM:SS
const regex = /^(\d{2}):(\d{2}):(\d{2})$/;
const match = duration.match(regex);
if (!match) return false;
const [hours, minutes, seconds] = match.slice(1).map(Number);
// Validate ranges: hours, minutes, and seconds
if (hours < 0 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) {
return false;
}
return true;
}
// ####################################################
// INIT CONFIG
// ####################################################
async function checkInitConfig() {
const localStorageInitConfig = lS.getLocalStorageInitConfig();
console.log('04.5 ----> Get init config', localStorageInitConfig);
if (localStorageInitConfig) {
if (isAudioVideoAllowed && !localStorageInitConfig.audioVideo) {
await handleAudioVideo();
} else {
if (isAudioAllowed && !localStorageInitConfig.audio) handleAudio();
if (isVideoAllowed && !localStorageInitConfig.video) handleVideo();
}
}
}
// ####################################################
// SOME PEER INFO
// ####################################################
function getPeerInfo() {
peer_info = {
join_data_time: getDataTimeString(),
peer_uuid: peer_uuid,
peer_id: socket.id,
peer_name: peer_name,
peer_avatar: peer_avatar,
peer_token: peer_token,
peer_presenter: isPresenter,
peer_audio: isAudioAllowed,
peer_audio_volume: 100,
peer_video: isVideoAllowed,
peer_screen: isScreenAllowed,
peer_recording: isRecording,
peer_video_privacy: isVideoPrivacyActive,
peer_hand: false,
is_desktop_device: isDesktopDevice,
is_mobile_device: isMobileDevice,
is_tablet_device: isTabletDevice,
is_ipad_pro_device: isIPadDevice,
os_name: parserResult.os.name,
os_version: parserResult.os.version,
browser_name: parserResult.browser.name,
browser_version: parserResult.browser.version,
user_agent: userAgent,
};
}
function getInfo() {
try {
console.log('Info', parserResult);
const filterUnknown = (obj) => {
const filtered = {};
for (const [key, value] of Object.entries(obj)) {
if (value && value !== 'Unknown') {
filtered[key] = value;
}
}
return filtered;
};
const filteredResult = {
//ua: parserResult.ua,
browser: filterUnknown(parserResult.browser),
cpu: filterUnknown(parserResult.cpu),
device: filterUnknown(parserResult.device),
engine: filterUnknown(parserResult.engine),
os: filterUnknown(parserResult.os),
};
// Convert the filtered result to a readable JSON string
const resultString = JSON.stringify(filteredResult, null, 2);
extraInfo.innerText = resultString;
return parserResult;
} catch (error) {
console.error('Error parsing user agent:', error);
}
}
// ####################################################
// ENTER YOUR NAME | Enable/Disable AUDIO/VIDEO
// ####################################################
async function whoAreYou() {
console.log('04 ----> Who are you?');
document.body.style.background = 'var(--body-bg)';
try {
const response = await axios.get('/config', {
timeout: 5000,
});
const serverButtons = response.data.message;
if (serverButtons) {
// Merge serverButtons into BUTTONS, keeping the existing keys in BUTTONS if they are not present in serverButtons
BUTTONS = {
...BUTTONS, // Spread current BUTTONS first to keep existing keys
...serverButtons, // Overwrite or add new keys from serverButtons
};
console.log('04 ----> AXIOS ROOM BUTTONS SETTINGS', {
serverButtons: serverButtons,
clientButtons: BUTTONS,
});
}
} catch (error) {
console.error('04 ----> AXIOS GET CONFIG ERROR', error.message);
}
if (navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia) {
BUTTONS.main.startScreenButton && show(initStartScreenButton);
}
// Virtual Background if supported (Chrome/Edge/Opera/Vivaldi/...)
if (
isMediaStreamTrackAndTransformerSupported &&
(BUTTONS.settings.virtualBackground !== undefined ? BUTTONS.settings.virtualBackground : true)
) {
show(initVirtualBackgroundButton);
show(videoVirtualBackground);
}
if (peer_name) {
hide(loadingDiv);
checkMedia();
getPeerInfo();
joinRoom(peer_name, room_id);
return;
}
let default_name = window.localStorage.peer_name ? window.localStorage.peer_name : '';
if (getCookie(room_id + '_name')) {
default_name = getCookie(room_id + '_name');
}
if (!BUTTONS.main.startVideoButton) {
isVideoAllowed = false;
elemDisplay('initVideo', false);
elemDisplay('initVideoButton', false);
elemDisplay('initAudioVideoButton', false);
elemDisplay('initVideoAudioRefreshButton', false);
elemDisplay('initVideoSelect', false);
elemDisplay('tabVideoDevicesBtn', false);
initVideoContainerShow(false);
}
if (!BUTTONS.main.startAudioButton) {
isAudioAllowed = false;
elemDisplay('initAudioButton', false);
elemDisplay('initAudioVideoButton', false);
elemDisplay('initVideoAudioRefreshButton', false);
elemDisplay('initMicrophoneSelect', false);
elemDisplay('initSpeakerSelect', false);
elemDisplay('tabAudioDevicesBtn', false);
}
if (!BUTTONS.main.startScreenButton) {
hide(initStartScreenButton);
}
// Fetch the OIDC profile and manage peer_name
let force_peer_name = false;
try {
const { data: profile } = await axios.get('/profile', { timeout: 5000 });
if (profile) {
console.log('AXIOS GET OIDC Profile retrieved successfully', profile);
// Define peer_name based on the profile properties and preferences
const peerNamePreference = profile.peer_name || {};
default_name =
(peerNamePreference.email && profile.email) ||
(peerNamePreference.name && profile.name) ||
default_name;
// Set localStorage and force_peer_name if applicable
if (default_name && peerNamePreference.force) {
window.localStorage.peer_name = default_name;
force_peer_name = true;
}
} else {
console.warn('AXIOS GET Profile data is empty or undefined');
}
} catch (error) {
console.error('AXIOS OIDC Error fetching profile', error.message || error);
}
initUser.classList.toggle('hidden');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swalBackground,
title: BRAND.app?.name,
input: 'text',
inputPlaceholder: 'Enter your email or name',
inputAttributes: { maxlength: 254, id: 'usernameInput' },
inputValue: default_name,
html: initUser, // Inject HTML
confirmButtonText: `Join meeting`,
customClass: { popup: 'init-modal-size' },
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
willOpen: () => {
hide(loadingDiv);
},
inputValidator: (name) => {
if (!name) return 'Please enter your email or name';
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(name);
if ((isEmail && name.length > 254) || (!isEmail && name.length > 32)) {
return isEmail ? 'Email must be max 254 char' : 'Name must be max 32 char';
}
name = filterXSS(name);
if (isHtml(name)) return 'Invalid name!';
if (!getCookie(room_id + '_name')) {
window.localStorage.peer_name = name;
}
setCookie(room_id + '_name', name, 30);
peer_name = name;
},
}).then(async () => {
if (!usernameEmoji.classList.contains('hidden')) {
usernameEmoji.classList.add('hidden');
}
if (initStream && !joinRoomWithScreen) {
await stopTracks(initStream);
elemDisplay('initVideo', false);
initVideoContainerShow(false);
}
getPeerInfo();
joinRoom(peer_name, room_id);
});
if (force_peer_name) {
getId('usernameInput').disabled = true;
hide(initUsernameEmojiButton);
}
if (!isVideoAllowed) {
elemDisplay('initVideo', false);
initVideoContainerShow(false);
hide(initVideoSelect);
}
if (!isAudioAllowed) {
hide(initMicrophoneSelect);
hide(initSpeakerSelect);
}
}
function handleAudio() {
isAudioAllowed = isAudioAllowed ? false : true;
initAudioButton.className = 'fas fa-microphone' + (isAudioAllowed ? '' : '-slash');
setColor(initAudioButton, isAudioAllowed ? 'white' : 'red');
setColor(startAudioButton, isAudioAllowed ? 'white' : 'red');
checkInitAudio(isAudioAllowed);
lS.setInitConfig(lS.MEDIA_TYPE.audio, isAudioAllowed);
}
function handleVideo() {
isVideoAllowed = isVideoAllowed ? false : true;
initVideoButton.className = 'fas fa-video' + (isVideoAllowed ? '' : '-slash');
setColor(initVideoButton, isVideoAllowed ? 'white' : 'red');
setColor(startVideoButton, isVideoAllowed ? 'white' : 'red');
checkInitVideo(isVideoAllowed);
lS.setInitConfig(lS.MEDIA_TYPE.video, isVideoAllowed);
elemDisplay('imageGrid', false);
isVideoAllowed &&
isMediaStreamTrackAndTransformerSupported &&
(BUTTONS.settings.virtualBackground !== undefined ? BUTTONS.settings.virtualBackground : true)
? show(initVirtualBackgroundButton)
: hide(initVirtualBackgroundButton);
}
async function handleAudioVideo() {
isAudioVideoAllowed = isAudioVideoAllowed ? false : true;
isAudioAllowed = isAudioVideoAllowed;
isVideoAllowed = isAudioVideoAllowed;
lS.setInitConfig(lS.MEDIA_TYPE.audio, isAudioVideoAllowed);
lS.setInitConfig(lS.MEDIA_TYPE.video, isAudioVideoAllowed);
lS.setInitConfig(lS.MEDIA_TYPE.audioVideo, isAudioVideoAllowed);
initAudioButton.className = 'fas fa-microphone' + (isAudioVideoAllowed ? '' : '-slash');
initVideoButton.className = 'fas fa-video' + (isAudioVideoAllowed ? '' : '-slash');
initAudioVideoButton.className = 'fas fa-eye' + (isAudioVideoAllowed ? '' : '-slash');
if (!isAudioVideoAllowed) {
hide(initAudioButton);
hide(initVideoButton);
hide(initVideoAudioRefreshButton);
}
if (isAudioAllowed && isVideoAllowed && !isMobileDevice) show(initVideoAudioRefreshButton);
setColor(initAudioVideoButton, isAudioVideoAllowed ? 'white' : 'red');
setColor(initAudioButton, isAudioAllowed ? 'white' : 'red');
setColor(initVideoButton, isVideoAllowed ? 'white' : 'red');
setColor(startAudioButton, isAudioAllowed ? 'white' : 'red');
setColor(startVideoButton, isVideoAllowed ? 'white' : 'red');
await checkInitVideo(isVideoAllowed);
checkInitAudio(isAudioAllowed);
elemDisplay('imageGrid', false);
isVideoAllowed &&
isMediaStreamTrackAndTransformerSupported &&
(BUTTONS.settings.virtualBackground !== undefined ? BUTTONS.settings.virtualBackground : true)
? show(initVirtualBackgroundButton)
: hide(initVirtualBackgroundButton);
}
async function checkInitVideo(isVideoAllowed) {
if (isVideoAllowed && BUTTONS.main.startVideoButton) {
if (initVideoSelect.value) {
initVideoContainerShow();
await changeCamera(initVideoSelect.value);
}
sound('joined');
} else {
if (initStream) {
stopTracks(initStream);
elemDisplay('initVideo', false);
initVideoContainerShow(false);
sound('left');
}
}
initVideoSelect.disabled = !isVideoAllowed;
}
function checkInitAudio(isAudioAllowed) {
initMicrophoneSelect.disabled = !isAudioAllowed;
initSpeakerSelect.disabled = !isAudioAllowed;
isAudioAllowed ? sound('joined') : sound('left');
}
function initVideoContainerShow(show = true) {
initVideoContainerClass.style.width = show ? '100%' : 'auto';
initVideoContainerClass.style.padding = show ? '10px' : '0px';
}
function checkMedia() {
let audio = getQueryParam('audio');
let video = getQueryParam('video');
if (audio) {
audio = audio.toLowerCase();
let queryPeerAudio = audio === '1' || audio === 'true';
if (queryPeerAudio != null) isAudioAllowed = queryPeerAudio;
}
if (video) {
video = video.toLowerCase();
let queryPeerVideo = video === '1' || video === 'true';
if (queryPeerVideo != null) isVideoAllowed = queryPeerVideo;
}
// elemDisplay('tabVideoDevicesBtn', isVideoAllowed);
// elemDisplay('tabAudioDevicesBtn', isAudioAllowed);
console.log('Direct join', {
audio: isAudioAllowed,
video: isVideoAllowed,
});
}
// ####################################################
// SHARE ROOM
// ####################################################
async function shareRoom(useNavigator = false) {
if (navigator.share && useNavigator) {
try {
await navigator.share({ url: RoomURL });
userLog('info', 'Room Shared successfully', 'top-end');
} catch (err) {
share();
}
} else {
share();
}
function share() {
sound('open');
Swal.fire({
background: swalBackground,
position: 'center',
title: 'Share the room',
html: `
Join from your mobile device
No need for apps, simply capture the QR code with your mobile camera Or Invite someone else to join by sending them the following URL
${RoomURL}
`, showDenyButton: true, showCancelButton: true, cancelButtonColor: 'red', denyButtonColor: 'green', confirmButtonText: `Copy URL`, denyButtonText: `Email invite`, cancelButtonText: `Close`, showClass: { popup: 'animate__animated animate__fadeInDown' }, hideClass: { popup: 'animate__animated animate__fadeOutUp' }, }).then((result) => { if (result.isConfirmed) { copyRoomURL(); } else if (result.isDenied) { shareRoomByEmail(); } // share screen on join if (isScreenAllowed) { rc.shareScreen(); } }); makeRoomQR(); } } // #################################################### // ROOM UTILITY // #################################################### function makeRoomQR() { const qr = new QRious({ element: document.getElementById('qrRoom'), value: RoomURL, }); qr.set({ size: 256, }); } function makeRoomPopupQR() { const qr = new QRious({ element: document.getElementById('qrRoomPopup'), value: RoomURL, }); qr.set({ size: 256, }); } function copyRoomURL() { let tmpInput = document.createElement('input'); document.body.appendChild(tmpInput); tmpInput.value = RoomURL; tmpInput.select(); tmpInput.setSelectionRange(0, 99999); // For mobile devices navigator.clipboard.writeText(tmpInput.value); document.body.removeChild(tmpInput); userLog('info', 'Meeting URL copied to clipboard 👍', 'top-end'); } function copyToClipboard(txt, showTxt = true) { let tmpInput = document.createElement('input'); document.body.appendChild(tmpInput); tmpInput.value = txt; tmpInput.select(); tmpInput.setSelectionRange(0, 99999); // For mobile devices navigator.clipboard.writeText(tmpInput.value); document.body.removeChild(tmpInput); showTxt ? userLog('info', `${txt} copied to clipboard 👍`, 'top-end') : userLog('info', `Copied to clipboard 👍`, 'top-end'); } function shareRoomByEmail() { Swal.fire({ allowOutsideClick: false, allowEscapeKey: false, background: swalBackground, imageUrl: image.email, position: 'center', title: 'Select a Date and Time', html: '', showCancelButton: true, confirmButtonText: 'OK', cancelButtonColor: 'red', showClass: { popup: 'animate__animated animate__fadeInDown' }, hideClass: { popup: 'animate__animated animate__fadeOutUp' }, preConfirm: () => { const newLine = '%0D%0A%0D%0A'; const selectedDateTime = document.getElementById('datetimePicker').value; const roomPassword = isRoomLocked && (room_password || rc.RoomPassword) ? 'Password: ' + (room_password || rc.RoomPassword) + newLine : ''; const email = ''; const emailSubject = `Please join our ${BRAND.app.name} Video Chat Meeting`; const emailBody = `The meeting is scheduled at: ${newLine} DateTime: ${selectedDateTime} ${newLine}${roomPassword}Click to join: ${RoomURL} ${newLine}`; document.location = 'mailto:' + email + '?subject=' + emailSubject + '&body=' + emailBody; }, }); flatpickr('#datetimePicker', { enableTime: true, dateFormat: 'Y-m-d H:i', time_24hr: true, }); } // #################################################### // JOIN ROOM // #################################################### function joinRoom(peer_name, room_id) { if (rc && rc.isConnected()) { console.log('Already connected to a room'); } else { console.log('05 ----> join Room ' + room_id); roomId.innerText = room_id; userName.innerText = peer_name; isUserPresenter.innerText = isPresenter; rc = new RoomClient( localAudio, remoteAudios, videoMediaContainer, videoPinMediaContainer, window.mediasoupClient, socket, room_id, peer_name, peer_uuid, peer_info, isAudioAllowed, isVideoAllowed, isScreenAllowed, joinRoomWithScreen, isSpeechSynthesisSupported, transcription, roomIsReady ); handleRoomClientEvents(); } } function roomIsReady() { startRoomSession(); makeRoomPopupQR(); if (peer_avatar && isImageURL(peer_avatar)) { myProfileAvatar.setAttribute('src', peer_avatar); } else if (rc.isValidEmail(peer_name)) { myProfileAvatar.style.borderRadius = `50px`; myProfileAvatar.setAttribute('src', rc.genGravatar(peer_name)); } else { myProfileAvatar.setAttribute('src', rc.genAvatarSvg(peer_name, 64)); } const controlDiv = getId('control'); if (controlDiv) { const visibleButtons = Array.from(controlDiv.children).filter( (el) => el.offsetParent !== null && !el.classList.contains('hidden') ); BUTTONS.main.extraButton || visibleButtons.length > 0 ? show(toggleExtraButton) : hide(toggleExtraButton); } else { show(toggleExtraButton); } BUTTONS.main.exitButton && show(exitButton); BUTTONS.main.shareButton && show(shareButton); BUTTONS.main.hideMeButton && show(hideMeButton); if (BUTTONS.settings.tabRecording) { show(startRecButton); } else { hide(startRecButton); hide(tabRecordingBtn); } BUTTONS.main.chatButton && show(chatButton); BUTTONS.main.pollButton && show(pollButton); BUTTONS.main.editorButton && show(editorButton); BUTTONS.main.raiseHandButton && show(raiseHandButton); BUTTONS.main.emojiRoomButton && show(emojiRoomButton); !BUTTONS.chat.chatSaveButton && hide(chatSaveButton); BUTTONS.chat.chatEmojiButton && show(chatEmojiButton); BUTTONS.chat.chatMarkdownButton && show(chatMarkdownButton); !BUTTONS.poll.pollSaveButton && hide(pollSaveButton); speechRecognition && BUTTONS.chat.chatSpeechStartButton ? show(chatSpeechStartButton) : (BUTTONS.chat.chatSpeechStartButton = false); transcription.isSupported() && BUTTONS.main.transcriptionButton ? show(transcriptionButton) : (BUTTONS.main.transcriptionButton = false); show(chatCleanTextButton); show(chatPasteButton); show(chatSendButton); if (isDesktopDevice) { show(whiteboardGridBtn); } if (isMobileDevice) { hide(initVideoAudioRefreshButton); hide(refreshVideoDevices); hide(refreshAudioDevices); BUTTONS.main.swapCameraButton && show(swapCameraButton); rc.chatMaximize(); hide(chatTogglePin); hide(chatMaxButton); hide(chatMinButton); rc.pollMaximize(); hide(pollTogglePin); hide(editorTogglePin); hide(pollMaxButton); hide(pollMinButton); transcription.maximize(); hide(transcriptionTogglePinBtn); hide(transcriptionMaxBtn); hide(transcriptionMinBtn); } else { rc.makeDraggable(emojiPickerContainer, emojiPickerHeader); rc.makeDraggable(chatRoom, chatHeader); rc.makeDraggable(pollRoom, pollHeader); //rc.makeDraggable(editorRoom, editorHeader); rc.makeDraggable(mySettings, mySettingsHeader); rc.makeDraggable(whiteboard, whiteboardHeader); rc.makeDraggable(sendFileDiv, imgShareSend); rc.makeDraggable(receiveFileDiv, imgShareReceive); rc.makeDraggable(lobby, lobbyHeader); rc.makeDraggable(transcriptionRoom, transcriptionHeader); if (navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia) { if (BUTTONS.main.startScreenButton) { show(startScreenButton); show(ScreenQualityDiv); show(ScreenFpsDiv); } BUTTONS.main.snapshotRoomButton && show(snapshotRoomButton); } BUTTONS.chat.chatPinButton && show(chatTogglePin); BUTTONS.chat.chatMaxButton && show(chatMaxButton); BUTTONS.poll.pollPinButton && show(pollTogglePin); show(editorTogglePin); BUTTONS.poll.pollMaxButton && show(pollMaxButton); BUTTONS.settings.pushToTalk && show(pushToTalkDiv); BUTTONS.settings.tabRTMPStreamingBtn && show(tabRTMPStreamingBtn) && show(startRtmpButton) && show(startRtmpURLButton) && show(streamerRtmpButton); } if (BUTTONS.main.fullScreenButton && !parserResult.browser.name.toLowerCase().includes('safari')) { document.onfullscreenchange = () => { if (!document.fullscreenElement) rc.isDocumentOnFullScreen = false; }; show(fullScreenButton); } else { hide(fullScreenButton); } BUTTONS.main.whiteboardButton && show(whiteboardButton); if (BUTTONS.main.documentPiPButton && showDocumentPipBtn) show(documentPiPButton); BUTTONS.main.settingsButton && show(settingsButton); isAudioAllowed ? show(stopAudioButton) : BUTTONS.main.startAudioButton && show(startAudioButton); isVideoAllowed ? show(stopVideoButton) : BUTTONS.main.startVideoButton && show(startVideoButton); BUTTONS.settings.fileSharing && show(fileShareButton); BUTTONS.settings.lockRoomButton && show(lockRoomButton); BUTTONS.settings.broadcastingButton && show(broadcastingButton); BUTTONS.settings.lobbyButton && show(lobbyButton); BUTTONS.settings.sendEmailInvitation && show(sendEmailInvitation); if (rc.recording.recSyncServerRecording) show(roomRecordingServer); BUTTONS.main.aboutButton && show(aboutButton); if (!isMobileDevice) show(pinUnpinGridDiv); if (!isSpeechSynthesisSupported) hide(speechMsgDiv); if ( isMediaStreamTrackAndTransformerSupported && (BUTTONS.settings.virtualBackground !== undefined ? BUTTONS.settings.virtualBackground : true) ) { rc.showVideoImageSelector(); } handleButtons(); handleSelects(); handleInputs(); handleChatEmojiPicker(); handleRoomEmojiPicker(); handleEditor(); loadSettingsFromLocalStorage(); startSessionTimer(); handleButtonsBar(); checkButtonsBar(); if (room_password) { lockRoomButton.click(); } //show(restartICEButton); // TEST } // #################################################### // UTILS // #################################################### function elemDisplay(elem, display, mode = 'block') { elem = getId(elem); if (!elem) { elementNotFound(elem); return; } elem.style.display = display ? mode : 'none'; } function hide(elem) { elem = typeof elem === 'string' ? getId(elem) : elem; if (!elem || !elem.classList) { elementNotFound(elem); return; } if (!elem.classList.contains('hidden')) elem.classList.toggle('hidden'); } function show(elem) { elem = typeof elem === 'string' ? getId(elem) : elem; if (!elem || !elem.classList) { elementNotFound(elem); return; } if (elem.classList.contains('hidden')) elem.classList.toggle('hidden'); } function disable(elem, disabled) { elem = typeof elem === 'string' ? getId(elem) : elem; if (!elem) { elementNotFound(elem); return; } elem.disabled = disabled; } function setColor(elem, color) { elem = typeof elem === 'string' ? getId(elem) : elem; if (!elem) { elementNotFound(elem); return; } elem.style.color = color; } function getColor(elem) { elem = typeof elem === 'string' ? getId(elem) : elem; if (!elem) { elementNotFound(elem); return undefined; } return elem.style.color; } function elementNotFound(element) { console.error('Element Not Found', element); return false; } // #################################################### // SESSION TIMER // #################################################### function startSessionTimer() { sessionTime.style.display = 'inline'; let callStartTime = Date.now(); setInterval(function printTime() { let callElapsedTime = Date.now() - callStartTime; sessionTime.innerText = getTimeToString(callElapsedTime); }, 1000); } function getTimeToString(time) { let diffInHrs = time / 3600000; let hh = Math.floor(diffInHrs); let diffInMin = (diffInHrs - hh) * 60; let mm = Math.floor(diffInMin); let diffInSec = (diffInMin - mm) * 60; let ss = Math.floor(diffInSec); let formattedHH = hh.toString().padStart(2, '0'); let formattedMM = mm.toString().padStart(2, '0'); let formattedSS = ss.toString().padStart(2, '0'); return `${formattedHH}:${formattedMM}:${formattedSS}`; } // #################################################### // RECORDING TIMER // #################################################### function secondsToHms(d) { d = Number(d); let h = Math.floor(d / 3600); let m = Math.floor((d % 3600) / 60); let s = Math.floor((d % 3600) % 60); let hDisplay = h > 0 ? h + 'h' : ''; let mDisplay = m > 0 ? m + 'm' : ''; let sDisplay = s > 0 ? s + 's' : ''; return hDisplay + ' ' + mDisplay + ' ' + sDisplay; } function startRecordingTimer() { recElapsedTime = 0; recTimer = setInterval(function printTime() { if (rc.isRecording()) { recElapsedTime++; recordingStatus.innerText = secondsToHms(recElapsedTime); } }, 1000); } function stopRecordingTimer() { clearInterval(recTimer); } // #################################################### // HTML BUTTONS // #################################################### function handleButtons() { // Lobby... document.getElementById('lobbyUsers').addEventListener('click', function (event) { switch (event.target.id) { case 'lobbyAcceptAllBtn': rc.lobbyAcceptAll(); break; case 'lobbyRejectAllBtn': rc.lobbyRejectAll(); break; default: break; } }); control.onmouseover = () => { isButtonsBarOver = true; }; control.onmouseout = () => { isButtonsBarOver = false; }; bottomButtons.onmouseover = () => { isButtonsBarOver = true; }; bottomButtons.onmouseout = () => { isButtonsBarOver = false; }; exitButton.onclick = () => { leaveRoom(); }; shareButton.onclick = () => { shareRoom(true); }; shareButton.onmouseenter = () => { if (isMobileDevice || !BUTTONS.popup.shareRoomQrOnHover) return; show(qrRoomPopupContainer); }; shareButton.onmouseleave = () => { if (isMobileDevice || !BUTTONS.popup.shareRoomQrOnHover) return; hide(qrRoomPopupContainer); }; hideMeButton.onclick = (e) => { if (isHideALLVideosActive) { return userLog('warning', 'To use this feature, please toggle video focus mode', 'top-end', 6000); } isHideMeActive = !isHideMeActive; rc.handleHideMe(); hideClassElements('videoMenuBar'); }; settingsButton.onclick = () => { rc.toggleMySettings(); }; mySettingsCloseBtn.onclick = () => { rc.toggleMySettings(); }; tabVideoDevicesBtn.onclick = (e) => { rc.openTab(e, 'tabVideoDevices'); }; tabAudioDevicesBtn.onclick = (e) => { rc.openTab(e, 'tabAudioDevices'); }; tabRecordingBtn.onclick = (e) => { rc.openTab(e, 'tabRecording'); }; tabRoomBtn.onclick = (e) => { rc.openTab(e, 'tabRoom'); }; tabVideoShareBtn.onclick = (e) => { rc.openTab(e, 'tabVideoShare'); }; tabRTMPStreamingBtn.onclick = (e) => { rc.getRTMP(); rc.openTab(e, 'tabRTMPStreaming'); }; refreshVideoFiles.onclick = () => { rc.getRTMP(); userLog('info', 'Refreshed video files', 'top-end'); }; tabAspectBtn.onclick = (e) => { rc.openTab(e, 'tabAspect'); }; tabModeratorBtn.onclick = (e) => { rc.openTab(e, 'tabModerator'); }; tabProfileBtn.onclick = (e) => { rc.openTab(e, 'tabProfile'); }; tabShortcutsBtn.onclick = (e) => { rc.openTab(e, 'tabShortcuts'); }; tabStylingBtn.onclick = (e) => { rc.openTab(e, 'tabStyling'); }; tabLanguagesBtn.onclick = (e) => { rc.openTab(e, 'tabLanguages'); }; tabVideoAIBtn.onclick = (e) => { rc.openTab(e, 'tabVideoAI'); rc.getAvatarList(); rc.getVoiceList(); }; avatarVideoAIStart.onclick = (e) => { rc.stopSession(); rc.handleVideoAI(); rc.toggleMySettings(); }; switchAvatars.onchange = (e) => { showFreeAvatars = e.currentTarget.checked; rc.getAvatarList(); }; avatarQuality.selectedIndex = 1; avatarQuality.onchange = (e) => { VideoAI.quality = e.target.value; }; refreshVideoDevices.onclick = async () => { await refreshMyVideoDevices(); userLog('info', 'Refreshed video devices', 'top-end'); }; refreshAudioDevices.onclick = async () => { await refreshMyAudioDevices(); userLog('info', 'Refreshed audio devices', 'top-end'); }; speakerTestBtn.onclick = () => { sound('ring', true); }; roomId.onclick = () => { isMobileDevice ? shareRoom(true) : copyRoomURL(); }; roomSendEmail.onclick = () => { shareRoomByEmail(); }; chatButton.onclick = () => { rc.toggleChat(); if (isMobileDevice) { rc.toggleShowParticipants(); } }; // Polls pollButton.onclick = () => { rc.togglePoll(); }; pollMaxButton.onclick = () => { rc.pollMaximize(); }; pollMinButton.onclick = () => { rc.pollMinimize(); }; pollCloseBtn.onclick = () => { rc.togglePoll(); }; pollTogglePin.onclick = () => { rc.togglePollPin(); }; pollSaveButton.onclick = () => { rc.pollSaveResults(); }; pollAddOptionBtn.onclick = () => { rc.pollAddOptions(); }; pollDelOptionBtn.onclick = () => { rc.pollDeleteOptions(); }; pollCreateForm.onsubmit = (e) => { rc.pollCreateNewForm(e); }; editorButton.onclick = () => { rc.toggleEditor(); if (isPresenter && !rc.editorIsLocked()) { rc.editorSendAction('open'); } }; editorCloseBtn.onclick = () => { rc.toggleEditor(); if (isPresenter && !rc.editorIsLocked()) { rc.editorSendAction('close'); } }; editorTogglePin.onclick = () => { rc.toggleEditorPin(); }; editorLockBtn.onclick = () => { rc.toggleLockUnlockEditor(); }; editorUnlockBtn.onclick = () => { rc.toggleLockUnlockEditor(); }; editorCleanBtn.onclick = () => { rc.editorClean(); }; editorCopyBtn.onclick = () => { rc.editorCopy(); }; editorSaveBtn.onclick = () => { rc.editorSave(); }; editorUndoBtn.onclick = () => { rc.editorUndo(); }; editorRedoBtn.onclick = () => { rc.editorRedo(); }; transcriptionButton.onclick = () => { transcription.toggle(); }; transcriptionCloseBtn.onclick = () => { transcription.toggle(); }; transcriptionTogglePinBtn.onclick = () => { transcription.togglePinUnpin(); }; transcriptionMaxBtn.onclick = () => { transcription.maximize(); }; transcriptionMinBtn.onclick = () => { transcription.minimize(); }; transcriptionAllBtn.onclick = () => { transcription.startAll(); }; transcriptionGhostBtn.onclick = () => { transcription.toggleBg(); }; transcriptionSaveBtn.onclick = () => { transcription.save(); }; transcriptionCleanBtn.onclick = () => { transcription.delete(); }; chatHideParticipantsList.onclick = (e) => { rc.toggleShowParticipants(); }; chatShowParticipantsList.onclick = (e) => { rc.toggleShowParticipants(); }; chatShareRoomBtn.onclick = (e) => { shareRoom(true); }; chatGhostButton.onclick = (e) => { rc.chatToggleBg(); }; chatCleanButton.onclick = () => { rc.chatClean(); }; chatSaveButton.onclick = () => { rc.chatSave(); }; chatCloseButton.onclick = () => { rc.toggleChat(); }; chatTogglePin.onclick = () => { rc.toggleChatPin(); }; chatMaxButton.onclick = () => { rc.chatMaximize(); }; chatMinButton.onclick = () => { rc.chatMinimize(); }; chatCleanTextButton.onclick = () => { rc.cleanMessage(); }; chatPasteButton.onclick = () => { rc.pasteMessage(); }; chatSendButton.onclick = () => { rc.sendMessage(); }; chatEmojiButton.onclick = () => { rc.toggleChatEmoji(); }; chatMarkdownButton.onclick = () => { isChatMarkdownOn = !isChatMarkdownOn; setColor(chatMarkdownButton, isChatMarkdownOn ? 'lime' : 'white'); }; chatSpeechStartButton.onclick = () => { startSpeech(); }; chatSpeechStopButton.onclick = () => { stopSpeech(); }; transcriptionSpeechStart.onclick = () => { transcription.start(); }; transcriptionSpeechStop.onclick = () => { transcription.stop(); }; fullScreenButton.onclick = () => { rc.toggleRoomFullScreen(); }; recordingImage.onclick = () => { isRecording ? stopRecButton.click() : startRecButton.click(); }; startRecButton.onclick = () => { rc.startRecording(); }; stopRecButton.onclick = () => { rc.stopRecording(); }; pauseRecButton.onclick = () => { rc.pauseRecording(); }; resumeRecButton.onclick = () => { rc.resumeRecording(); }; swapCameraButton.onclick = () => { if (isHideMeActive) rc.handleHideMe(); rc.closeThenProduce(RoomClient.mediaType.video, null, true); }; raiseHandButton.onclick = () => { rc.updatePeerInfo(peer_name, socket.id, 'hand', true); hideClassElements('videoMenuBar'); }; lowerHandButton.onclick = () => { rc.updatePeerInfo(peer_name, socket.id, 'hand', false); }; toggleExtraButton.onclick = () => { toggleExtraButtons(); if (!isMobileDevice) { isToggleExtraBtnClicked = true; setTimeout(() => { isToggleExtraBtnClicked = false; }, 2000); } }; toggleExtraButton.onmouseover = () => { if (isToggleExtraBtnClicked || isMobileDevice) return; if (control.style.display === 'none') { toggleExtraButtons(); } }; startAudioButton.onclick = async () => { const moderator = rc.getModerator(); if (moderator.audio_cant_unmute) { return userLog('warning', 'The moderator does not allow you to unmute', 'top-end', 6000); } if (isPushToTalkActive) return; setAudioButtonsDisabled(true); if (!isEnumerateAudioDevices) await initEnumerateAudioDevices(); const producerExist = rc.producerExist(RoomClient.mediaType.audio); console.log('START AUDIO producerExist --->', producerExist); producerExist ? await rc.resumeProducer(RoomClient.mediaType.audio) : await rc.produce(RoomClient.mediaType.audio, microphoneSelect.value); rc.updatePeerInfo(peer_name, socket.id, 'audio', true); }; stopAudioButton.onclick = async () => { if (isPushToTalkActive) return; setAudioButtonsDisabled(true); const producerExist = rc.producerExist(RoomClient.mediaType.audio); console.log('STOP AUDIO producerExist --->', producerExist); producerExist ? await rc.pauseProducer(RoomClient.mediaType.audio) : await rc.closeProducer(RoomClient.mediaType.audio); rc.updatePeerInfo(peer_name, socket.id, 'audio', false); }; startVideoButton.onclick = async () => { const moderator = rc.getModerator(); if (moderator.video_cant_unhide) { return userLog('warning', 'The moderator does not allow you to unhide', 'top-end', 6000); } setVideoButtonsDisabled(true); if (!isEnumerateVideoDevices) await initEnumerateVideoDevices(); await rc.produce(RoomClient.mediaType.video, videoSelect.value); // await rc.resumeProducer(RoomClient.mediaType.video); }; stopVideoButton.onclick = () => { setVideoButtonsDisabled(true); rc.closeProducer(RoomClient.mediaType.video); // await rc.pauseProducer(RoomClient.mediaType.video); }; startScreenButton.onclick = async () => { const moderator = rc.getModerator(); if (moderator.screen_cant_share) { return userLog('warning', 'The moderator does not allow you to share the screen', 'top-end', 6000); } await rc.produce(RoomClient.mediaType.screen); }; stopScreenButton.onclick = () => { rc.closeProducer(RoomClient.mediaType.screen); }; copyRtmpUrlButton.onclick = () => { rc.copyRTMPUrl(rtmpLiveUrl.value); }; startRtmpButton.onclick = () => { if (rc.selectedRtmpFilename == '') { userLog('warning', 'Please select the Video file to stream', 'top-end', 6000); return; } rc.startRTMP(); }; stopRtmpButton.onclick = () => { rc.stopRTMP(); }; streamerRtmpButton.onclick = () => { rc.openRTMPStreamer(); }; startRtmpURLButton.onclick = () => { rc.startRTMPfromURL(rtmpStreamURL.value); }; stopRtmpURLButton.onclick = () => { rc.stopRTMPfromURL(); }; fileShareButton.onclick = () => { rc.selectFileToShare(socket.id, true); }; videoShareButton.onclick = () => { rc.shareVideo('all'); }; videoCloseBtn.onclick = () => { if (rc._moderator.media_cant_sharing) { return userLog('warning', 'The moderator does not allow you close this media', 'top-end', 6000); } rc.closeVideo(true); }; sendAbortBtn.onclick = () => { rc.abortFileTransfer(); }; receiveAbortBtn.onclick = () => { rc.abortReceiveFileTransfer(); }; receiveHideBtn.onclick = () => { rc.hideFileTransfer(); }; whiteboardButton.onclick = () => { toggleWhiteboard(); }; documentPiPButton.onclick = () => { rc.toggleDocumentPIP(); }; snapshotRoomButton.onclick = () => { rc.snapshotRoom(); }; whiteboardPencilBtn.onclick = () => { whiteboardIsDrawingMode(true); }; whiteboardObjectBtn.onclick = () => { whiteboardIsDrawingMode(false); }; whiteboardUndoBtn.onclick = () => { whiteboardAction(getWhiteboardAction('undo')); }; whiteboardRedoBtn.onclick = () => { whiteboardAction(getWhiteboardAction('redo')); }; whiteboardSaveBtn.onclick = () => { wbCanvasSaveImg(); }; whiteboardImgFileBtn.onclick = () => { whiteboardAddObj('imgFile'); }; whiteboardPdfFileBtn.onclick = () => { whiteboardAddObj('pdfFile'); }; whiteboardImgUrlBtn.onclick = () => { whiteboardAddObj('imgUrl'); }; whiteboardTextBtn.onclick = () => { whiteboardAddObj('text'); }; whiteboardLineBtn.onclick = () => { whiteboardAddObj('line'); }; whiteboardRectBtn.onclick = () => { whiteboardAddObj('rect'); }; whiteboardTriangleBtn.onclick = () => { whiteboardAddObj('triangle'); }; whiteboardCircleBtn.onclick = () => { whiteboardAddObj('circle'); }; whiteboardEraserBtn.onclick = () => { whiteboardIsEraser(true); }; whiteboardCleanBtn.onclick = () => { confirmClearBoard(); }; whiteboardCloseBtn.onclick = () => { whiteboardAction(getWhiteboardAction('close')); }; whiteboardLockBtn.onclick = () => { toggleLockUnlockWhiteboard(); }; whiteboardUnlockBtn.onclick = () => { toggleLockUnlockWhiteboard(); }; participantsSaveBtn.onclick = () => { saveRoomPeers(); }; participantsUnreadMessagesBtn.onclick = () => { rc.toggleUnreadMsg(); }; participantsRaiseHandBtn.onclick = () => { rc.toggleRaiseHands(); }; searchParticipantsFromList.onkeyup = () => { rc.searchPeer(); }; lockRoomButton.onclick = () => { rc.roomAction('lock'); }; unlockRoomButton.onclick = () => { rc.roomAction('unlock'); }; aboutButton.onclick = () => { showAbout(); }; restartICEButton.onclick = async () => { await rc.restartIce(); }; } // #################################################### // HANDLE INIT USER // #################################################### function setButtonsInit() { if (!isMobileDevice) { setTippy('initAudioButton', 'Toggle the audio', 'top'); setTippy('initVideoButton', 'Toggle the video', 'top'); setTippy('initAudioVideoButton', 'Toggle the audio & video', 'top'); setTippy('initStartScreenButton', 'Toggle screen sharing', 'top'); setTippy('initStopScreenButton', 'Toggle screen sharing', 'top'); setTippy('initVideoMirrorButton', 'Toggle video mirror', 'top'); setTippy('initVirtualBackgroundButton', 'Set Virtual Background or Blur', 'top'); setTippy('initUsernameEmojiButton', 'Toggle username emoji', 'top'); } if (!isAudioAllowed) hide(initAudioButton); if (!isVideoAllowed) hide(initVideoButton); if (!isAudioAllowed || !isVideoAllowed) hide(initAudioVideoButton); if ((!isAudioAllowed && !isVideoAllowed) || isMobileDevice) hide(initVideoAudioRefreshButton); isAudioVideoAllowed = isAudioAllowed && isVideoAllowed; } function handleSelectsInit() { // devices init options initVideoSelect.onchange = async () => { await changeCamera(initVideoSelect.value); videoSelect.selectedIndex = initVideoSelect.selectedIndex; refreshLsDevices(); }; initMicrophoneSelect.onchange = () => { microphoneSelect.selectedIndex = initMicrophoneSelect.selectedIndex; refreshLsDevices(); }; initSpeakerSelect.onchange = () => { speakerSelect.selectedIndex = initSpeakerSelect.selectedIndex; refreshLsDevices(); }; } async function setSelectsInit() { if (localStorageDevices) { console.log('04.0 ----> Get Local Storage Devices before', localStorageDevices); // const initMicrophoneExist = selectOptionByValueExist(initMicrophoneSelect, localStorageDevices.audio.select); const initSpeakerExist = selectOptionByValueExist(initSpeakerSelect, localStorageDevices.speaker.select); const initVideoExist = selectOptionByValueExist(initVideoSelect, localStorageDevices.video.select); // const microphoneExist = selectOptionByValueExist(microphoneSelect, localStorageDevices.audio.select); const speakerExist = selectOptionByValueExist(speakerSelect, localStorageDevices.speaker.select); const videoExist = selectOptionByValueExist(videoSelect, localStorageDevices.video.select); console.log('Check for audio changes', { previous: localStorageDevices.audio.select, current: microphoneSelect.value, }); if (!initMicrophoneExist || !microphoneExist) { console.log('04.1 ----> Audio devices seems changed, use default index 0'); initMicrophoneSelect.selectedIndex = 0; microphoneSelect.selectedIndex = 0; refreshLsDevices(); } console.log('Check for speaker changes', { previous: localStorageDevices.speaker.select, current: speakerSelect.value, }); if (!initSpeakerExist || !speakerExist) { console.log('04.2 ----> Speaker devices seems changed, use default index 0'); initSpeakerSelect.selectedIndex = 0; speakerSelect.selectedIndex = 0; refreshLsDevices(); } console.log('Check for video changes', { previous: localStorageDevices.video.select, current: videoSelect.value, }); if (!initVideoExist || !videoExist) { console.log('04.3 ----> Video devices seems changed, use default index 0'); initVideoSelect.selectedIndex = 0; videoSelect.selectedIndex = 0; refreshLsDevices(); } // console.log('04.4 ----> Get Local Storage Devices after', lS.getLocalStorageDevices()); } if (initVideoSelect.value) await changeCamera(initVideoSelect.value); } function selectOptionByValueExist(selectElement, value) { let foundValue = false; for (let i = 0; i < selectElement.options.length; i++) { if (selectElement.options[i].value === value) { selectElement.selectedIndex = i; foundValue = true; break; } } return foundValue; } function refreshLsDevices() { lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value); lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, microphoneSelect.selectedIndex, microphoneSelect.value); lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, speakerSelect.selectedIndex, speakerSelect.value); } async function changeCamera(deviceId) { if (initStream) { await stopTracks(initStream); elemDisplay('initVideo', true); initVideoContainerShow(); } const videoConstraints = { audio: false, video: { width: { ideal: 1280 }, height: { ideal: 720 }, deviceId: { exact: deviceId }, aspectRatio: 1.777, }, }; await navigator.mediaDevices .getUserMedia(videoConstraints) .then(async (camStream) => { initVideo.srcObject = camStream; initStream = camStream; console.log( '04.5 ----> Success attached init cam video stream', initStream.getVideoTracks()[0].getSettings() ); checkInitConfig(); camera = detectCameraFacingMode(camStream); handleCameraMirror(initVideo); }) .catch((error) => { console.error('[Error] changeCamera', error); handleMediaError('video/audio', error, '/'); }); if (isVideoAllowed) { await loadVirtualBackgroundSettings(); } } function detectCameraFacingMode(stream) { if (!stream || !stream.getVideoTracks().length) { console.warn("No video track found in the stream. Defaulting to 'user'."); return 'user'; } const videoTrack = stream.getVideoTracks()[0]; const settings = videoTrack.getSettings(); const capabilities = videoTrack.getCapabilities?.() || {}; const facingMode = settings.facingMode || capabilities.facingMode?.[0] || 'user'; return facingMode === 'environment' ? 'environment' : 'user'; } // #################################################### // HANDLE MEDIA ERROR // #################################################### function handleMediaError(mediaType, err, redirectURL = false) { sound('alert'); let errMessage = err; let getUserMediaError = true; switch (err.name) { case 'NotFoundError': case 'DevicesNotFoundError': errMessage = 'Required track is missing'; break; case 'NotReadableError': case 'TrackStartError': errMessage = 'Already in use'; break; case 'OverconstrainedError': case 'ConstraintNotSatisfiedError': errMessage = 'Constraints cannot be satisfied by available devices'; if (videoQuality.selectedIndex != 0) { videoQuality.selectedIndex = rc.videoQualitySelectedIndex; } break; case 'NotAllowedError': case 'PermissionDeniedError': errMessage = 'Permission denied in browser'; break; case 'TypeError': errMessage = 'Empty constraints object'; break; default: getUserMediaError = false; break; } let html = `Error message:
${errMessage}
Drag and drop your file here