Files
mirotalk/public/js/client.js
T
2025-08-05 17:54:59 +02:00

11821 lines
393 KiB
JavaScript

/*
██████ ██  ██ ███████ ███  ██ ████████ 
██      ██  ██ ██      ████  ██    ██    
██  ██  ██ █████  ██ ██  ██  ██ 
██  ██  ██ ██     ██  ██ ██  ██ 
██████ ███████ ██ ███████ ██   ████  ██ 
*/
/**
* MiroTalk P2P - Client component
*
* @link GitHub: https://github.com/miroslavpejic85/mirotalk
* @link Official Live demo: https://p2p.mirotalk.com
* @license For open source use: AGPLv3
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.5.48
*
*/
'use strict';
// https://www.w3schools.com/js/js_strict.asp
// Signaling server URL
const signalingServer = getSignalingServer();
// This room
const myRoomId = getId('myRoomId');
const roomId = getRoomId();
const myRoomUrl = window.location.origin + '/join/' + roomId; // share room url
// Images
const images = {
caption: '../images/caption.png',
chatgpt: '../images/chatgpt.png',
confirmation: '../images/image-placeholder.png',
share: '../images/share.png',
locked: '../images/locked.png',
videoOff: '../images/cam-off.png',
audioOff: '../images/audio-off.png',
audioGif: '../images/audio.gif',
delete: '../images/delete.png',
message: '../images/message.png',
leave: '../images/leave-room.png',
vaShare: '../images/va-share.png',
about: '../images/mirotalk-logo.gif',
feedback: '../images/feedback.png',
forbidden: '../images/forbidden.png',
avatar: '../images/mirotalk-logo.png',
recording: '../images/recording.png',
poster: '../images/loader.gif',
geoLocation: '../images/geolocation.png',
}; // nice free icon: https://www.iconfinder.com
const className = {
user: 'fas fa-user',
clock: 'fas fa-clock',
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',
screenOn: 'fas fa-desktop',
screenOff: 'fas fa-stop-circle',
handPulsate: 'fas fa-hand-paper pulsate',
privacy: 'far fa-circle',
snapShot: 'fas fa-camera-retro',
pinUnpin: 'fas fa-map-pin',
mirror: 'fas fa-arrow-right-arrow-left',
zoomIn: 'fas fa-magnifying-glass-plus',
zoomOut: 'fas fa-magnifying-glass-minus',
fullScreen: 'fas fa-expand',
fsOn: 'fas fa-compress-alt',
fsOff: 'fas fa-expand-alt',
msgPrivate: 'fas fa-paper-plane',
geoLocation: 'fas fa-location-dot',
shareFile: 'fas fa-upload',
shareVideoAudio: 'fab fa-youtube',
kickOut: 'fas fa-sign-out-alt',
chatOn: 'fas fa-comment',
chatOff: 'fas fa-comment-slash',
ghost: 'fas fa-ghost',
undo: 'fas fa-undo',
captionOn: 'fas fa-closed-captioning',
trash: 'fas fa-trash',
copy: 'fas fa-copy',
speech: 'fas fa-volume-high',
heart: 'fas fa-heart',
pip: 'fas fa-images',
hideAll: 'fas fa-eye',
up: 'fas fa-chevron-up',
down: 'fas fa-chevron-down',
};
// https://fontawesome.com/search?o=r&m=free
const icons = {
lock: '<i class="fas fa-lock"></i>',
unlock: '<i class="fas fa-lock-open"></i>',
pitchBar: '<i class="fas fa-microphone-lines"></i>',
sounds: '<i class="fas fa-music"></i>',
share: '<i class="fas fa-share-alt"></i>',
user: '<i class="fas fa-user"></i>',
fileSend: '<i class="fas fa-file-export"></i>',
fileReceive: '<i class="fas fa-file-import"></i>',
codecs: '<i class="fa-solid fa-film"></i>',
theme: '<i class="fas fa-fill-drip"></i>',
};
// Whiteboard and fileSharing
const fileSharingInput = '*'; // allow all file extensions
const Base64Prefix = 'data:application/pdf;base64,';
const wbPdfInput = 'application/pdf';
const wbImageInput = 'image/*';
const wbWidth = 1280;
const wbHeight = 768;
// Peer infos
const extraInfo = getId('extraInfo');
const isWebRTCSupported = checkWebRTCSupported();
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 osName = parserResult.os.name;
const osVersion = parserResult.os.version;
const browserName = parserResult.browser.name;
const browserVersion = parserResult.browser.version;
const isFirefox = browserName.toLowerCase().includes('firefox');
const peerInfo = getPeerInfo();
const thisInfo = getInfo();
// Local Storage class
const lS = new LocalStorage();
const localStorageSettings = lS.getObjectLocalStorage('P2P_SETTINGS');
const lsSettings = localStorageSettings ? localStorageSettings : lS.P2P_SETTINGS;
console.log('LOCAL_STORAGE_SETTINGS', lsSettings);
// Check if embedded inside an iFrame
const isEmbedded = window.self !== window.top;
// Check if PIP is supported by this browser
const showVideoPipBtn = document.pictureInPictureEnabled;
// Check if Document PIP is supported by this browser
const showDocumentPipBtn = !isEmbedded && 'documentPictureInPicture' in window;
// Loading div
const loadingDiv = getId('loadingDiv');
// Video/Audio media container
const videoMediaContainer = getId('videoMediaContainer');
const videoPinMediaContainer = getId('videoPinMediaContainer');
const audioMediaContainer = getId('audioMediaContainer');
// Share Room QR popup
const qrRoomPopupContainer = getId('qrRoomPopupContainer');
// Init audio-video
const initUser = getId('initUser');
const initVideoContainer = getQs('.init-video-container');
const initVideo = getId('initVideo');
const initVideoBtn = getId('initVideoBtn');
const initAudioBtn = getId('initAudioBtn');
const initScreenShareBtn = getId('initScreenShareBtn');
const initVideoMirrorBtn = getId('initVideoMirrorBtn');
const initUsernameEmojiButton = getId('initUsernameEmojiButton');
const initVideoSelect = getId('initVideoSelect');
const initMicrophoneSelect = getId('initMicrophoneSelect');
const initSpeakerSelect = getId('initSpeakerSelect');
const usernameEmoji = getId('usernameEmoji');
// Buttons bar
const buttonsBar = getId('buttonsBar');
const shareRoomBtn = getId('shareRoomBtn');
const recordStreamBtn = getId('recordStreamBtn');
const fullScreenBtn = getId('fullScreenBtn');
const chatRoomBtn = getId('chatRoomBtn');
const captionBtn = getId('captionBtn');
const roomEmojiPickerBtn = getId('roomEmojiPickerBtn');
const whiteboardBtn = getId('whiteboardBtn');
const snapshotRoomBtn = getId('snapshotRoomBtn');
const fileShareBtn = getId('fileShareBtn');
const documentPiPBtn = getId('documentPiPBtn');
const mySettingsBtn = getId('mySettingsBtn');
const aboutBtn = getId('aboutBtn');
// Buttons bottom
const bottomButtons = getId('bottomButtons');
const toggleExtraBtn = getId('toggleExtraBtn');
const audioBtn = getId('audioBtn');
const videoBtn = getId('videoBtn');
const swapCameraBtn = getId('swapCameraBtn');
const hideMeBtn = getId('hideMeBtn');
const screenShareBtn = getId('screenShareBtn');
const myHandBtn = getId('myHandBtn');
const leaveRoomBtn = getId('leaveRoomBtn');
// Room Emoji Picker
const closeEmojiPickerContainer = getId('closeEmojiPickerContainer');
const emojiPickerContainer = getId('emojiPickerContainer');
const emojiPickerHeader = getId('emojiPickerHeader');
const userEmoji = getId(`userEmoji`);
// Chat room
const msgerDraggable = getId('msgerDraggable');
const msgerHeader = getId('msgerHeader');
const msgerTogglePin = getId('msgerTogglePin');
const msgerTheme = getId('msgerTheme');
const msgerCPBtn = getId('msgerCPBtn');
const msgerDropDownMenuBtn = getId('msgerDropDownMenuBtn');
const msgerDropDownContent = getId('msgerDropDownContent');
const msgerClean = getId('msgerClean');
const msgerSaveBtn = getId('msgerSaveBtn');
const msgerClose = getId('msgerClose');
const msgerMaxBtn = getId('msgerMaxBtn');
const msgerMinBtn = getId('msgerMinBtn');
const msgerChat = getId('msgerChat');
const msgerEmojiBtn = getId('msgerEmojiBtn');
const msgerMarkdownBtn = getId('msgerMarkdownBtn');
const msgerGPTBtn = getId('msgerGPTBtn');
const msgerShareFileBtn = getId('msgerShareFileBtn');
const msgerVideoUrlBtn = getId('msgerVideoUrlBtn');
const msgerInput = getId('msgerInput');
const msgerCleanTextBtn = getId('msgerCleanTextBtn');
const msgerPasteBtn = getId('msgerPasteBtn');
const msgerShowChatOnMsgDiv = getId('msgerShowChatOnMsgDiv');
const msgerShowChatOnMsg = getId('msgerShowChatOnMsg');
const msgerSpeechMsgDiv = getId('msgerSpeechMsgDiv');
const msgerSpeechMsg = getId('msgerSpeechMsg');
const msgerSendBtn = getId('msgerSendBtn');
const chatInputEmoji = {
'<3': '❤️',
'</3': '💔',
':D': '😀',
':)': '😃',
';)': '😉',
':(': '😒',
':p': '😛',
';p': '😜',
":'(": '😢',
':+1:': '👍',
':*': '😘',
':O': '😲',
':|': '😐',
':*(': '😭',
XD: '😆',
':B': '😎',
':P': '😜',
'<(': '👎',
'>:(': '😡',
':S': '😟',
':X': '🤐',
';(': '😥',
':T': '😖',
':@': '😠',
':$': '🤑',
':&': '🤗',
':#': '🤔',
':!': '😵',
':W': '😷',
':%': '🤒',
':*!': '🤩',
':G': '😬',
':R': '😋',
':M': '🤮',
':L': '🥴',
':C': '🥺',
':F': '🥳',
':Z': '🤢',
':^': '🤓',
':K': '🤫',
':D!': '🤯',
':H': '🧐',
':U': '🤥',
':V': '🤪',
':N': '🥶',
':J': '🥴',
}; // https://github.com/wooorm/gemoji/blob/main/support.md
// Chat room emoji picker
const msgerEmojiPicker = getId('msgerEmojiPicker');
// Chat room connected peers
const msgerCP = getId('msgerCP');
const msgerCPHeader = getId('msgerCPHeader');
const msgerCPCloseBtn = getId('msgerCPCloseBtn');
const msgerCPList = getId('msgerCPList');
const searchPeerBarName = getId('searchPeerBarName');
// Caption section
const captionDraggable = getId('captionDraggable');
const captionHeader = getId('captionHeader');
const captionTogglePin = getId('captionTogglePin');
const captionTheme = getId('captionTheme');
const captionMaxBtn = getId('captionMaxBtn');
const captionMinBtn = getId('captionMinBtn');
const captionClean = getId('captionClean');
const captionSaveBtn = getId('captionSaveBtn');
const captionClose = getId('captionClose');
const captionChat = getId('captionChat');
const captionFooter = getId('captionFooter');
// My settings
const mySettings = getId('mySettings');
const mySettingsHeader = getId('mySettingsHeader');
const tabVideoBtn = getId('tabVideoBtn');
const tabAudioBtn = getId('tabAudioBtn');
const tabVideoShareBtn = getId('tabVideoShareBtn');
const tabRecordingBtn = getId('tabRecordingBtn');
const tabParticipantsBtn = getId('tabParticipantsBtn');
const tabProfileBtn = getId('tabProfileBtn');
const tabShortcutsBtn = getId('tabShortcutsBtn');
const tabNetworkBtn = getId('tabNetworkBtn');
const networkIP = getId('networkIP');
const networkHost = getId('networkHost');
const networkStun = getId('networkStun');
const networkTurn = getId('networkTurn');
const tabRoomBtn = getId('tabRoomBtn');
const roomSendEmailBtn = getId('roomSendEmailBtn');
const tabStylingBtn = getId('tabStylingBtn');
const tabLanguagesBtn = getId('tabLanguagesBtn');
const mySettingsCloseBtn = getId('mySettingsCloseBtn');
const myPeerNameSet = getId('myPeerNameSet');
const myPeerNameSetBtn = getId('myPeerNameSetBtn');
const switchSounds = getId('switchSounds');
const switchShare = getId('switchShare');
const switchKeepButtonsVisible = getId('switchKeepButtonsVisible');
const switchPushToTalk = getId('switchPushToTalk');
const switchAudioPitchBar = getId('switchAudioPitchBar');
const audioInputSelect = getId('audioSource');
const audioOutputSelect = getId('audioOutput');
const audioOutputDiv = getId('audioOutputDiv');
const speakerTestBtn = getId('speakerTestBtn');
const videoSelect = getId('videoSource');
const videoQualitySelect = getId('videoQuality');
const videoFpsSelect = getId('videoFps');
const videoFpsDiv = getId('videoFpsDiv');
const screenFpsSelect = getId('screenFps');
const pushToTalkDiv = getId('pushToTalkDiv');
const recImage = getId('recImage');
const switchH264Recording = getId('switchH264Recording');
const pauseRecBtn = getId('pauseRecBtn');
const resumeRecBtn = getId('resumeRecBtn');
const recordingTime = getId('recordingTime');
const lastRecordingInfo = getId('lastRecordingInfo');
const themeSelect = getId('mirotalkTheme');
const videoObjFitSelect = getId('videoObjFitSelect');
const mainButtonsBar = getQsA('#buttonsBar button');
const mainButtonsIcon = getQsA('#buttonsBar button i');
const btnsBarSelect = getId('mainButtonsBarPosition');
const pinUnpinGridDiv = getId('pinUnpinGridDiv');
const pinVideoPositionSelect = getId('pinVideoPositionSelect');
const tabRoomPeerName = getId('tabRoomPeerName');
const tabRoomParticipants = getId('tabRoomParticipants');
const tabRoomSecurity = getId('tabRoomSecurity');
const tabEmailInvitation = getId('tabEmailInvitation');
const isPeerPresenter = getId('isPeerPresenter');
const peersCount = getId('peersCount');
const screenFpsDiv = getId('screenFpsDiv');
const switchShortcuts = getId('switchShortcuts');
// Audio options
const micOptionsDiv = getId('micOptionsDiv');
const switchNoiseSuppression = getId('switchNoiseSuppression');
const labelNoiseSuppression = getId('labelNoiseSuppression');
// Tab Media
const shareMediaAudioVideoBtn = getId('shareMediaAudioVideoBtn');
// My whiteboard
const whiteboard = getId('whiteboard');
const whiteboardHeader = getId('whiteboardHeader');
const whiteboardTitle = getId('whiteboardTitle');
const whiteboardOptions = getId('whiteboardOptions');
const wbDrawingColorEl = getId('wbDrawingColorEl');
const whiteboardGhostButton = getId('whiteboardGhostButton');
const wbBackgroundColorEl = getId('wbBackgroundColorEl');
const whiteboardPencilBtn = getId('whiteboardPencilBtn');
const whiteboardObjectBtn = getId('whiteboardObjectBtn');
const whiteboardUndoBtn = getId('whiteboardUndoBtn');
const whiteboardRedoBtn = getId('whiteboardRedoBtn');
const whiteboardDropDownMenuBtn = getId('whiteboardDropDownMenuBtn');
const whiteboardDropdownMenu = getId('whiteboardDropdownMenu');
const whiteboardImgFileBtn = getId('whiteboardImgFileBtn');
const whiteboardPdfFileBtn = getId('whiteboardPdfFileBtn');
const whiteboardImgUrlBtn = getId('whiteboardImgUrlBtn');
const whiteboardTextBtn = getId('whiteboardTextBtn');
const whiteboardLineBtn = getId('whiteboardLineBtn');
const whiteboardRectBtn = getId('whiteboardRectBtn');
const whiteboardTriangleBtn = getId('whiteboardTriangleBtn');
const whiteboardCircleBtn = getId('whiteboardCircleBtn');
const whiteboardSaveBtn = getId('whiteboardSaveBtn');
const whiteboardEraserBtn = getId('whiteboardEraserBtn');
const whiteboardCleanBtn = getId('whiteboardCleanBtn');
const whiteboardLockBtn = getId('whiteboardLockBtn');
const whiteboardUnlockBtn = getId('whiteboardUnlockBtn');
const whiteboardCloseBtn = getId('whiteboardCloseBtn');
// Room actions buttons
const captionEveryoneBtn = getId('captionEveryoneBtn');
const muteEveryoneBtn = getId('muteEveryoneBtn');
const hideEveryoneBtn = getId('hideEveryoneBtn');
const ejectEveryoneBtn = getId('ejectEveryoneBtn');
const lockRoomBtn = getId('lockRoomBtn');
const unlockRoomBtn = getId('unlockRoomBtn');
// File send progress
const sendFileDiv = getId('sendFileDiv');
const imgShareSend = getId('imgShareSend');
const sendFilePercentage = getId('sendFilePercentage');
const sendFileInfo = getId('sendFileInfo');
const sendProgress = getId('sendProgress');
const sendAbortBtn = getId('sendAbortBtn');
// File receive progress
const receiveFileDiv = getId('receiveFileDiv');
const imgShareReceive = getId('imgShareReceive');
const receiveFilePercentage = getId('receiveFilePercentage');
const receiveFileInfo = getId('receiveFileInfo');
const receiveProgress = getId('receiveProgress');
const receiveHideBtn = getId('receiveHideBtn');
const receiveAbortBtn = getId('receiveAbortBtn');
// Video/audio url player
const videoUrlCont = getId('videoUrlCont');
const videoAudioUrlCont = getId('videoAudioUrlCont');
const videoUrlHeader = getId('videoUrlHeader');
const videoAudioUrlHeader = getId('videoAudioUrlHeader');
const videoUrlCloseBtn = getId('videoUrlCloseBtn');
const videoAudioCloseBtn = getId('videoAudioCloseBtn');
const videoUrlIframe = getId('videoUrlIframe');
const videoAudioUrlElement = getId('videoAudioUrlElement');
// Speech recognition
const speechRecognitionIcon = getId('speechRecognitionIcon');
const speechRecognitionStart = getId('speechRecognitionStart');
const speechRecognitionStop = getId('speechRecognitionStop');
// Media
const sinkId = 'sinkId' in HTMLMediaElement.prototype;
//....
const isRulesActive = true; // Presenter can do anything, guest is slightly moderate, if false no Rules for the room.
const forceCamMaxResolutionAndFps = false; // This force the webCam to max resolution as default, up to 8k and 60fps (very high bandwidth are required) if false, you can set it from settings
const useAvatarSvg = true; // if false the cam-Off avatar = images.avatar
/**
* Determines the video zoom mode.
* If set to true, the video zooms at the center of the frame.
* If set to false, the video zooms at the cursor position.
*/
const ZOOM_CENTER_MODE = false;
const ZOOM_IN_OUT_ENABLED = true; // Video Zoom in/out default (true)
// Color Picker:
const themeCustom = {
input: getId('themeColorPicker'),
check: getId('keepCustomTheme'),
color: lsSettings.theme_color ? lsSettings.theme_color : '#000000',
keep: lsSettings.theme_custom ? lsSettings.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: {
preview: true,
opacity: true,
hue: true,
},
})
.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', () => {
lsSettings.theme_color = themeCustom.color;
lS.setSettings(lsSettings);
});
// Room
let thisMaxRoomParticipants = 8;
// misc
let swBg = 'rgba(0, 0, 0, 0.7)'; // swAlert background color
let callElapsedTime; // count time
let mySessionTime; // conference session time
let isDocumentOnFullScreen = false;
let isToggleExtraBtnClicked = false;
// peer
let myPeerId; // This socket.id
let myPeerUUID = getUUID(); // Unique peer id
let myPeerName = getPeerName();
let myPeerAvatar = getPeerAvatar();
let myToken = getPeerToken(); // peer JWT
let isPresenter = false; // True Who init the room (aka first peer joined)
let myHandStatus = false;
let myVideoStatus = false;
let myAudioStatus = false;
let myVideoStatusBefore = false;
let myScreenStatus = false;
let isScreenEnabled = getScreenEnabled();
let notify = getNotify(); // popup room sharing on join
let notifyBySound = true; // turn on - off sound notifications
let isPeerReconnected = false;
// media
let useAudio = true; // User allow for microphone usage
let useVideo = true; // User allow for camera usage
let isEnumerateVideoDevices = false;
let isEnumerateAudioDevices = false;
// video/audio player
let isVideoUrlPlayerOpen = false;
let pinnedVideoPlayerId = null;
// connection
let signalingSocket; // socket.io connection to our webserver
let needToCreateOffer = false; // after session description answer
let peerConnections = {}; // keep track of our peer connections, indexed by peer_id == socket.io id
let chatDataChannels = {}; // keep track of our peer chat data channels
let fileDataChannels = {}; // keep track of our peer file sharing data channels
let allPeers = {}; // keep track of all peers in the room, indexed by peer_id == socket.io id
// stream
let initStream; // initial webcam stream
let localVideoMediaStream; // my webcam
let localAudioMediaStream; // my microphone
let noiseProcessor = null; // RNNoise audio processing
let peerVideoMediaElements = {}; // keep track of our peer <video> tags, indexed by peer_id_video
let peerAudioMediaElements = {}; // keep track of our peer <audio> tags, indexed by peer_id_audio
// main and bottom buttons
let mainButtonsBarPosition = 'vertical'; // vertical - horizontal
let placement = 'right'; // https://atomiks.github.io/tippyjs/#placements
let bottomButtonsPlacement = 'right';
let isButtonsVisible = false;
let isButtonsBarOver = false;
// video
let myVideo;
let myAudio;
let myVideoWrap;
let myVideoAvatarImage;
let myPrivacyBtn;
let myVideoPinBtn;
let myPitchBar;
let myVideoParagraph;
let myHandStatusIcon;
let myVideoStatusIcon;
let myAudioStatusIcon;
let isVideoPrivacyActive = false; // Video circle for privacy
let isVideoPinned = false;
let isVideoFullScreenSupported = true;
let isVideoOnFullScreen = false;
let isScreenSharingSupported = false;
let isScreenStreaming = false;
let isHideMeActive = getHideMeActive();
let remoteMediaControls = false; // enable - disable peers video player controls (default false)
let camera = 'user'; // user = front-facing camera on a smartphone. | environment = the back camera on a smartphone.
// chat
let leftChatAvatar;
let rightChatAvatar;
let chatMessagesId = 0;
let showChatOnMessage = true;
let isChatPinned = false;
let isCaptionPinned = false;
let isChatRoomVisible = false;
let isCaptionBoxVisible = false;
let isChatEmojiVisible = false;
let isChatMarkdownOn = false;
let isChatGPTOn = false;
let isChatPasteTxt = false;
let speechInMessages = false;
let isSpeechSynthesisSupported = 'speechSynthesis' in window;
let transcripts = []; // collect all the transcripts to save it later if you need
let chatMessages = []; // collect chat messages to save it later if want
let chatGPTcontext = []; // keep chatGPT messages context
// settings
let videoMaxFrameRate = 30;
let screenMaxFrameRate = 30;
let videoQualitySelectedIndex = 0; // default HD and 30fps
let videoFpsSelectedIndex = 1; // 30 fps
let screenFpsSelectedIndex = 1; // 30 fps
let isMySettingsVisible = false;
let thisRoomPassword = null;
let isRoomLocked = false;
let isKeepButtonsVisible = false;
let isAudioPitchBar = true;
let isPushToTalkActive = false;
let isSpaceDown = false;
let isShortcutsEnabled = false;
// recording
let mediaRecorder;
let recordedBlobs;
let audioRecorder; // helpers.js
let recScreenStream; // screen media to recording
let recTimer;
let recCodecs;
let recElapsedTime;
let recPrioritizeH264 = false;
let isStreamRecording = false;
let isStreamRecordingPaused = false;
let isRecScreenStream = false;
// whiteboard
let wbCanvas = null;
let wbIsLock = false;
let wbIsDrawing = false;
let wbIsOpen = false;
let wbIsRedoing = false;
let wbIsEraser = false;
let wbIsBgTransparent = false;
let wbPop = [];
let isWhiteboardFs = false;
// file transfer
let fileToSend;
let fileReader;
let receiveBuffer = [];
let receivedSize = 0;
let incomingFileInfo;
let incomingFileData;
let sendInProgress = false;
let receiveInProgress = false;
/**
* MTU 1kb/s to prevent drop.
* Note: FireFox seems not supports chunkSize > 1024?
*/
const chunkSize = 1024; // 1024 * 16; // 16kb/s
// server
let isHostProtected = false; // Username and Password required to initialize room
let isPeerAuthEnabled = false; // Username and Password required in the URL params to join room
// survey
let surveyActive = true; // when leaving the room give a feedback, if false will be redirected to newcall page
let surveyURL = 'https://www.questionpro.com/t/AUs7VZq00L';
// Redirect on leave room
let redirectActive = false;
let redirectURL = '/newcall';
// GeoLocation
const notificationService = new NotificationService({ Swal, swBg, images, playSound });
const geoService = GeoService;
let geo;
/**
* Load GeoLocation service
* @returns {void}
*/
function loadGeo() {
geo = new PeerGeoLocation({
room_id: roomId,
peer_name: myPeerName,
peer_id: myPeerId,
peer_uuid: myPeerUUID,
sendToServer,
msgPopup,
notificationService,
geoService,
openURL,
});
}
/**
* Load all Html elements by Id
*/
function getHtmlElementsById() {
mySessionTime = getId('mySessionTime');
// My video elements
myVideo = getId('myVideo');
myAudio = getId('myAudio');
myVideoWrap = getId('myVideoWrap');
myVideoAvatarImage = getId('myVideoAvatarImage');
myPrivacyBtn = getId('myPrivacyBtn');
myVideoPinBtn = getId('myVideoPinBtn');
myPitchBar = getId('myPitchBar');
// My username, hand/video/audio status
myVideoParagraph = getId('myVideoParagraph');
myHandStatusIcon = getId('myHandStatusIcon');
myVideoStatusIcon = getId('myVideoStatusIcon');
myAudioStatusIcon = getId('myAudioStatusIcon');
}
/**
* Using tippy aka very nice tooltip!
* https://atomiks.github.io/tippyjs/
*/
function setButtonsToolTip() {
// Not need for mobile
if (isMobileDevice) return;
// Init buttons
setTippy(initScreenShareBtn, 'Toggle screen sharing', 'top');
setTippy(initVideoMirrorBtn, 'Toggle video mirror', 'top');
setTippy(initUsernameEmojiButton, 'Toggle username emoji', 'top');
// Main buttons
refreshMainButtonsToolTipPlacement();
// Chat room buttons
setTippy(msgerClose, 'Close', 'bottom');
setTippy(msgerShowChatOnMsgDiv, 'Show chat when you receive a new message', 'bottom');
setTippy(msgerSpeechMsgDiv, 'Speech the incoming messages', 'bottom');
setTippy(msgerTogglePin, 'Toggle chat pin', 'bottom');
setTippy(msgerTheme, 'Ghost theme', 'bottom');
setTippy(msgerMaxBtn, 'Maximize', 'bottom');
setTippy(msgerMinBtn, 'Minimize', 'bottom');
setTippy(msgerEmojiBtn, 'Emoji', 'top');
setTippy(msgerMarkdownBtn, 'Markdown', 'top');
setTippy(msgerGPTBtn, 'ChatGPT', 'top');
setTippy(msgerShareFileBtn, 'Share file', 'top');
setTippy(msgerCPBtn, 'Private messages', 'top');
setTippy(msgerCleanTextBtn, 'Clean', 'top');
setTippy(msgerPasteBtn, 'Paste', 'top');
setTippy(msgerSendBtn, 'Send', 'top');
// Chat participants buttons
setTippy(msgerCPCloseBtn, 'Close', 'bottom');
// Caption buttons
setTippy(captionClose, 'Close', 'bottom');
setTippy(captionMaxBtn, 'Maximize', 'bottom');
setTippy(captionMinBtn, 'Minimize', 'bottom');
setTippy(captionTogglePin, 'Toggle caption pin', 'bottom');
setTippy(captionTheme, 'Ghost theme', 'bottom');
setTippy(captionClean, 'Clean the messages', 'bottom');
setTippy(captionSaveBtn, 'Save the messages', 'bottom');
setTippy(speechRecognitionIcon, 'Status', 'bottom');
setTippy(speechRecognitionStart, 'Start', 'top');
setTippy(speechRecognitionStop, 'Stop', 'top');
// Settings
setTippy(mySettingsCloseBtn, 'Close', 'bottom');
setTippy(myPeerNameSetBtn, 'Change name', 'top');
setTippy(myRoomId, 'Room name (click to copy/share)', '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 activated, on keyup will be deactivated, like a walkie-talkie',
'right'
);
setTippy(switchSounds, 'Toggle room notify sounds', 'right');
setTippy(switchShare, "Show 'Share Room' popup on join.", 'right');
setTippy(switchKeepButtonsVisible, 'Keep buttons always visible', 'right');
setTippy(recImage, 'Toggle recording', '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(networkIP, 'IP address associated with the ICE candidate', 'right');
setTippy(
networkHost,
'This type of ICE candidate represents a candidate that corresponds to an interface on the local device. Host candidates are typically generated based on the local IP addresses of the device and can be used for direct peer-to-peer communication within the same network',
'right'
);
setTippy(
networkStun,
'Server reflexive candidates are obtained by the ICE agent when it sends a request to a STUN (Session Traversal Utilities for NAT) server. These candidates reflect the public IP address and port of the client as observed by the STUN server. They are useful for traversing NATs (Network Address Translators) and establishing connectivity between peers across different networks',
'right'
);
setTippy(
networkTurn,
'Relay candidates are obtained when communication between peers cannot be established directly due to symmetric NATs or firewall restrictions. In such cases, communication is relayed through a TURN (Traversal Using Relays around NAT) server. TURN servers act as intermediaries, relaying data between peers, allowing them to communicate even when direct connections are not possible. This is typically the fallback mechanism for establishing connectivity when direct peer-to-peer communication fails',
'right'
);
// Whiteboard buttons
setTippy(whiteboardLockBtn, 'Toggle Lock whiteboard', 'right');
setTippy(whiteboardUnlockBtn, 'Toggle Lock whiteboard', 'right');
setTippy(whiteboardCloseBtn, 'Close', 'right');
setTippy(wbDrawingColorEl, 'Drawing color', 'bottom');
setTippy(whiteboardGhostButton, 'Toggle transparent background', 'bottom');
setTippy(wbBackgroundColorEl, 'Background color', 'bottom');
setTippy(whiteboardPencilBtn, 'Drawing mode', 'bottom');
setTippy(whiteboardObjectBtn, 'Object mode', 'bottom');
setTippy(whiteboardUndoBtn, 'Undo', 'bottom');
setTippy(whiteboardRedoBtn, 'Redo', 'bottom');
// Suspend/Hide File transfer buttons
setTippy(sendAbortBtn, 'Abort file transfer', 'bottom');
setTippy(receiveAbortBtn, 'Abort file transfer', 'bottom');
setTippy(receiveHideBtn, 'Hide file transfer', 'bottom');
// Video/audio URL player
setTippy(videoUrlCloseBtn, 'Close the video player', 'bottom');
setTippy(videoAudioCloseBtn, 'Close the video player', 'bottom');
setTippy(msgerVideoUrlBtn, 'Share a video or audio to all participants', 'top');
}
/**
* Refresh main buttons tooltips based of they position (vertical/horizontal)
* @returns void
*/
function refreshMainButtonsToolTipPlacement() {
// not need for mobile
if (isMobileDevice) return;
// ButtonsBar
placement = btnsBarSelect.options[btnsBarSelect.selectedIndex].value == 'vertical' ? 'right' : 'top';
// BottomButtons
bottomButtonsPlacement = btnsBarSelect.options[btnsBarSelect.selectedIndex].value == 'vertical' ? 'top' : 'right';
setTippy(shareRoomBtn, 'Share the Room', placement);
setTippy(hideMeBtn, 'Toggle hide myself from the room view', placement);
setTippy(recordStreamBtn, 'Start recording', placement);
setTippy(fullScreenBtn, 'View full screen', placement);
setTippy(captionBtn, 'Open the caption', placement);
setTippy(roomEmojiPickerBtn, 'Send reaction', placement);
setTippy(whiteboardBtn, 'Open the whiteboard', placement);
setTippy(snapshotRoomBtn, 'Snapshot screen, windows or tab', placement);
setTippy(fileShareBtn, 'Share file', placement);
setTippy(documentPiPBtn, 'Toggle Document picture in picture', placement);
setTippy(aboutBtn, 'About this project', placement);
setTippy(toggleExtraBtn, 'Toggle extra buttons', bottomButtonsPlacement);
setTippy(audioBtn, useAudio ? 'Stop the audio' : 'My audio is disabled', bottomButtonsPlacement);
setTippy(videoBtn, useVideo ? 'Stop the video' : 'My video is disabled', bottomButtonsPlacement);
setTippy(screenShareBtn, 'Start screen sharing', bottomButtonsPlacement);
setTippy(myHandBtn, 'Raise your hand', bottomButtonsPlacement);
setTippy(chatRoomBtn, 'Open the chat', bottomButtonsPlacement);
setTippy(mySettingsBtn, 'Open the settings', bottomButtonsPlacement);
setTippy(leaveRoomBtn, 'Leave this room', bottomButtonsPlacement);
}
/**
* Set nice tooltip to element
* @param {object} element element
* @param {string} content message to popup
* @param {string} placement position
*/
function setTippy(element, content, placement) {
if (isMobileDevice) return;
if (element) {
if (element._tippy) {
element._tippy.destroy();
}
try {
tippy(element, {
content: content,
placement: placement,
});
} catch (err) {
console.error('setTippy error', err.message);
}
} else {
console.warn('setTippy element not found with content', content);
}
}
/**
* Get peer info using D
* @returns {object} peer info
*/
function getPeerInfo() {
return {
isDesktopDevice: isDesktopDevice,
isMobileDevice: isMobileDevice,
isTabletDevice: isTabletDevice,
isIPadDevice: isIPadDevice,
osName: osName,
osVersion: osVersion,
browserName: browserName,
browserVersion: browserVersion,
};
}
/**
* Get Extra info
* @returns object info
*/
function getInfo() {
try {
console.log('Info', parserResult);
// Filter out properties with 'Unknown' values
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);
}
}
/**
* Get Signaling server URL
* @returns {string} Signaling server URL
*/
function getSignalingServer() {
console.log('00 Location', window.location);
return window.location.protocol + '//' + window.location.hostname;
}
/**
* Generate random Room id if not set
* @returns {string} Room Id
*/
function getRoomId() {
// check if passed as params /join?room=id
let qs = new URLSearchParams(window.location.search);
let queryRoomId = filterXSS(qs.get('room'));
// skip /join/
let roomId = queryRoomId ? queryRoomId : window.location.pathname.split('/join/')[1];
// if not specified room id, create one random
if (roomId == '') {
roomId = makeId(20);
const newUrl = signalingServer + '/join/' + roomId;
window.history.pushState({ url: newUrl }, roomId, newUrl);
}
console.log('Direct join', { room: roomId });
// Update Room name in settings
if (myRoomId) myRoomId.innerText = roomId;
// Save room name in local storage
window.localStorage.lastRoom = roomId;
return roomId;
}
/**
* Generate random Id
* @param {integer} length
* @returns {string} random id
*/
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;
}
/**
* Get UUID4
* @returns uuid4
*/
function getUUID() {
const uuid4 = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
if (window.localStorage.uuid) {
return window.localStorage.uuid;
}
window.localStorage.uuid = uuid4;
return uuid4;
}
/**
* Check if notify is set
* @returns {boolean} true/false (default true)
*/
function getNotify() {
let qs = new URLSearchParams(window.location.search);
let notify = filterXSS(qs.get('notify'));
if (notify) {
let queryNotify = notify === '1' || notify === 'true';
if (queryNotify != null) {
console.log('Direct join', { notify: queryNotify });
return queryNotify;
}
}
notify = lsSettings.share_on_join;
console.log('Direct join', { notify: notify });
return notify;
}
/**
* Get Peer JWT
* @returns {mixed} boolean false or token string
*/
function getPeerToken() {
if (window.sessionStorage.peer_token) return window.sessionStorage.peer_token;
let qs = new URLSearchParams(window.location.search);
let token = filterXSS(qs.get('token'));
let queryToken = false;
if (token) {
queryToken = token;
}
console.log('Direct join', { token: queryToken });
return queryToken;
}
/**
* Check if peer name is set
* @returns {string} Peer Name
*/
function getPeerName() {
const qs = new URLSearchParams(window.location.search);
const name = filterXSS(qs.get('name'));
if (isHtml(name)) {
console.log('Direct join', { name: 'Invalid name' });
return 'Invalid name';
}
console.log('Direct join', { name: name });
return name;
}
/**
* Check if peer avatar is set
* @returns {string} Peer Avatar
*/
function getPeerAvatar() {
const qs = new URLSearchParams(window.location.search);
const avatar = filterXSS(qs.get('avatar'));
const avatarDisabled = avatar === '0' || avatar === 'false';
console.log('Direct join', { avatar: avatar });
if (avatarDisabled || !isImageURL(avatar)) {
return false;
}
return avatar;
}
/**
* Is screen enabled on join room
* @returns {boolean} true/false
*/
function getScreenEnabled() {
let qs = new URLSearchParams(window.location.search);
let screen = filterXSS(qs.get('screen'));
if (screen) {
screen = screen.toLowerCase();
let queryPeerScreen = screen === '1' || screen === 'true';
console.log('Direct join', { screen: queryPeerScreen });
return queryPeerScreen;
}
console.log('Direct join', { screen: false });
return false;
}
/**
* Hide myself from the meeting view
* @returns {boolean} true/false
*/
function getHideMeActive() {
let qs = new URLSearchParams(window.location.search);
let hide = filterXSS(qs.get('hide'));
let queryHideMe = false;
if (hide) {
hide = hide.toLowerCase();
queryHideMe = hide === '1' || hide === 'true';
}
console.log('Direct join', { hide: queryHideMe });
return queryHideMe;
}
/**
* Check if there is peer connections
* @returns {boolean} true/false
*/
function thereArePeerConnections() {
if (Object.keys(peerConnections).length === 0) return false;
return true;
}
/**
* Count the peer connections
* @returns peer connections count
*/
function countPeerConnections() {
return Object.keys(peerConnections).length;
}
/**
* Get Started...
*/
document.addEventListener('DOMContentLoaded', function () {
initClientPeer();
});
/**
* On body load Get started
*/
function initClientPeer() {
setTheme();
if (!isWebRTCSupported) {
return userLog('error', 'This browser seems not supported WebRTC!');
}
// check if video Full screen supported on default true
if (peerInfo.isMobileDevice && peerInfo.osName === 'iOS') {
isVideoFullScreenSupported = false;
}
console.log('01. Connecting to signaling server');
// Disable the HTTP long-polling transport
signalingSocket = io({ transports: ['websocket'] });
const transport = signalingSocket.io.engine.transport.name; // in most cases, "polling"
console.log('02. Connection transport', transport);
// Check upgrade transport
signalingSocket.io.engine.on('upgrade', () => {
const upgradedTransport = signalingSocket.io.engine.transport.name; // in most cases, "websocket"
console.log('Connection upgraded transport', upgradedTransport);
});
// async - await requests
signalingSocket.request = function request(type, data = {}) {
return new Promise((resolve, reject) => {
signalingSocket.emit(type, data, (data) => {
if (data.error) {
console.error('signalingSocket.request error', data.error);
reject(data.error);
} else {
console.log('signalingSocket.request data', data);
resolve(data);
}
});
});
};
// on receiving data from signaling server...
signalingSocket.on('connect', handleConnect);
signalingSocket.on('unauthorized', handleUnauthorized);
signalingSocket.on('roomIsLocked', handleUnlockTheRoom);
signalingSocket.on('roomAction', handleRoomAction);
signalingSocket.on('addPeer', handleAddPeer);
signalingSocket.on('serverInfo', handleServerInfo);
signalingSocket.on('sessionDescription', handleSessionDescription);
signalingSocket.on('iceCandidate', handleIceCandidate);
signalingSocket.on('peerName', handlePeerName);
signalingSocket.on('peerStatus', handlePeerStatus);
signalingSocket.on('peerAction', handlePeerAction);
signalingSocket.on('cmd', handleCmd);
signalingSocket.on('message', handleMessage);
signalingSocket.on('wbCanvasToJson', handleJsonToWbCanvas);
signalingSocket.on('whiteboardAction', handleWhiteboardAction);
signalingSocket.on('caption', handleCaptionActions);
signalingSocket.on('kickOut', handleKickedOut);
signalingSocket.on('fileInfo', handleFileInfo);
signalingSocket.on('fileAbort', handleFileAbort);
signalingSocket.on('fileReceiveAbort', handleAbortFileTransfer);
signalingSocket.on('videoPlayer', handleVideoPlayer);
signalingSocket.on('disconnect', handleDisconnect);
signalingSocket.on('removePeer', handleRemovePeer);
} // end [initClientPeer]
/**
* Send async data to signaling server (server.js)
* @param {string} msg msg to send to signaling server
* @param {object} config data to send to signaling server
*/
async function sendToServer(msg, config = {}) {
await signalingSocket.emit(msg, config);
}
/**
* Send async data through RTC Data Channels
* @param {object} config data
*/
async function sendToDataChannel(config) {
if (thereArePeerConnections() && typeof config === 'object' && config !== null) {
for (let peer_id in chatDataChannels) {
if (chatDataChannels[peer_id].readyState === 'open')
await chatDataChannels[peer_id].send(JSON.stringify(config));
}
}
}
/**
* Connected to Signaling Server. Once the user has given us access to their
* microphone/cam, join the channel and start peering up
*/
async function handleConnect() {
console.log('03. Connected to signaling server');
myPeerId = signalingSocket.id;
console.log('04. My peer id [ ' + myPeerId + ' ]');
await getButtons();
if (localVideoMediaStream && localAudioMediaStream) {
await joinToChannel();
} else {
await initEnumerateDevices();
await setupLocalVideoMedia();
await setupLocalAudioMedia();
if (!useVideo || (!useVideo && !useAudio)) {
await loadLocalMedia(new MediaStream(), 'video');
}
getHtmlElementsById();
setButtonsToolTip();
handleUsernameEmojiPicker();
manageButtons();
handleButtonsRule();
setupMySettings();
loadSettingsFromLocalStorage();
setupVideoUrlPlayer();
startSessionTime();
await whoAreYou();
}
}
/**
* Handle some signaling server info
* @param {object} config data
*/
function handleServerInfo(config) {
console.log('13. Server info', config);
const { peers_count, host_protected, user_auth, is_presenter, survey, redirect, maxRoomParticipants } = config;
isHostProtected = host_protected;
isPeerAuthEnabled = user_auth;
// Get survey settings from server
surveyActive = survey.active;
surveyURL = survey.url;
// Get redirect settings from server
redirectActive = redirect.active;
redirectURL = redirect.url;
// Limit room to n peers
if (maxRoomParticipants) thisMaxRoomParticipants = maxRoomParticipants;
if (peers_count > thisMaxRoomParticipants) {
return roomIsBusy();
}
// Let start with some basic rules
isPresenter = isPeerReconnected ? isPresenter : is_presenter;
isPeerPresenter.innerText = isPresenter;
// Peer identified if presenter or not then....
handleShortcuts();
if (isRulesActive) {
handleRules(isPresenter);
}
if (notify && peers_count == 1) {
shareRoomMeetingURL(true);
} else {
checkShareScreen();
}
}
/**
* HOST_USER_AUTH enabled and peer not match valid username and password
*/
function handleUnauthorized() {
playSound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
imageUrl: images.forbidden,
title: 'Ops, 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=${roomId}`);
});
}
/**
* Room is busy, disconnect me and alert the user that
* will be redirected to home page
*/
function roomIsBusy() {
signalingSocket.disconnect();
playSound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
imageUrl: images.forbidden,
position: 'center',
title: 'Room is busy',
html: `The room is limited to ${thisMaxRoomParticipants} users. <br/> Please try again later`,
showDenyButton: false,
confirmButtonText: `OK`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
openURL('/');
}
});
}
/**
* Presenter can do anything, for others you can limit
* some functions by hidden the buttons etc.
*
* @param {boolean} isPresenter true/false
*/
function handleRules(isPresenter) {
console.log('14. Peer isPresenter: ' + isPresenter + ' Reconnected to signaling server: ' + isPeerReconnected);
if (!isPresenter) {
buttons.settings.showTabRoomParticipants = false;
buttons.settings.showTabRoomSecurity = false;
buttons.settings.showTabEmailInvitation = false;
buttons.remote.showKickOutBtn = false;
buttons.remote.showGeoLocationBtn = false;
buttons.whiteboard.whiteboardLockBtn = false;
//...
} else {
buttons.main.showShareRoomBtn = true;
buttons.settings.showMicOptionsBtn = true;
buttons.settings.showTabRoomParticipants = true;
buttons.settings.showTabRoomSecurity = true;
buttons.settings.showTabEmailInvitation = true;
buttons.settings.showLockRoomBtn = !isRoomLocked;
buttons.settings.showUnlockRoomBtn = isRoomLocked;
buttons.remote.audioBtnClickAllowed = true;
buttons.remote.videoBtnClickAllowed = true;
buttons.remote.showKickOutBtn = true;
buttons.whiteboard.whiteboardLockBtn = true;
}
handleButtonsRule();
}
/**
* Hide not desired buttons
*/
function handleButtonsRule() {
// Main
elemDisplay(shareRoomBtn, buttons.main.showShareRoomBtn);
elemDisplay(hideMeBtn, buttons.main.showHideMeBtn);
elemDisplay(
toggleExtraBtn,
buttons.main.showExtraBtn &&
Array.from(buttonsBar.children).filter((el) => el.style.display !== 'none').length > 0
);
elemDisplay(audioBtn, buttons.main.showAudioBtn);
elemDisplay(videoBtn, buttons.main.showVideoBtn);
//elemDisplay(screenShareBtn, buttons.main.showScreenBtn, ); // auto-detected
elemDisplay(recordStreamBtn, buttons.main.showRecordStreamBtn);
elemDisplay(recImage, buttons.main.showRecordStreamBtn);
elemDisplay(chatRoomBtn, buttons.main.showChatRoomBtn);
elemDisplay(captionBtn, buttons.main.showCaptionRoomBtn && speechRecognition); // auto-detected
elemDisplay(roomEmojiPickerBtn, buttons.main.showRoomEmojiPickerBtn);
elemDisplay(myHandBtn, buttons.main.showMyHandBtn);
elemDisplay(whiteboardBtn, buttons.main.showWhiteboardBtn);
elemDisplay(snapshotRoomBtn, buttons.main.showSnapshotRoomBtn && !isMobileDevice);
elemDisplay(fileShareBtn, buttons.main.showFileShareBtn);
elemDisplay(documentPiPBtn, showDocumentPipBtn && buttons.main.showDocumentPipBtn);
elemDisplay(mySettingsBtn, buttons.main.showMySettingsBtn);
elemDisplay(aboutBtn, buttons.main.showAboutBtn);
// chat
elemDisplay(msgerTogglePin, !isMobileDevice && buttons.chat.showTogglePinBtn);
elemDisplay(msgerMaxBtn, !isMobileDevice && buttons.chat.showMaxBtn);
elemDisplay(msgerSaveBtn, buttons.chat.showSaveMessageBtn);
elemDisplay(msgerMarkdownBtn, buttons.chat.showMarkDownBtn);
elemDisplay(msgerGPTBtn, buttons.chat.showChatGPTBtn);
elemDisplay(msgerShareFileBtn, buttons.chat.showFileShareBtn);
elemDisplay(msgerVideoUrlBtn, buttons.chat.showShareVideoAudioBtn);
elemDisplay(msgerCPBtn, buttons.chat.showParticipantsBtn);
// caption
elemDisplay(captionTogglePin, !isMobileDevice && buttons.caption.showTogglePinBtn);
elemDisplay(captionMaxBtn, !isMobileDevice && buttons.caption.showMaxBtn);
// Settings
elemDisplay(micOptionsDiv, buttons.settings.showMicOptionsBtn || isPresenter);
elemDisplay(captionEveryoneBtn, buttons.settings.showCaptionEveryoneBtn);
elemDisplay(muteEveryoneBtn, buttons.settings.showMuteEveryoneBtn);
elemDisplay(hideEveryoneBtn, buttons.settings.showHideEveryoneBtn);
elemDisplay(ejectEveryoneBtn, buttons.settings.showEjectEveryoneBtn);
elemDisplay(lockRoomBtn, buttons.settings.showLockRoomBtn);
elemDisplay(unlockRoomBtn, buttons.settings.showUnlockRoomBtn);
elemDisplay(tabRoomPeerName, buttons.settings.showTabRoomPeerName);
elemDisplay(tabRoomParticipants, buttons.settings.showTabRoomParticipants);
elemDisplay(tabRoomSecurity, buttons.settings.showTabRoomSecurity);
elemDisplay(tabEmailInvitation, buttons.settings.showTabEmailInvitation);
// Whiteboard
buttons.whiteboard.whiteboardLockBtn
? elemDisplay(whiteboardLockBtn, true, 'flex')
: elemDisplay(whiteboardLockBtn, false);
}
/**
* Get Buttons config from server side and apply to current client
*/
async function getButtons() {
try {
const response = await axios.get('/buttons', {
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('AXIOS ROOM BUTTONS SETTINGS', {
serverButtons: serverButtons,
clientButtons: buttons,
});
}
} catch (error) {
console.error('AXIOS GET CONFIG ERROR', error.message);
}
}
/**
* Get user name from OIDC profile
* @returns {string} Peer Name
*/
async function getUserName() {
try {
const { data: profile } = await axios.get('/profile', { timeout: 5000 });
if (profile && profile.name) {
console.log('AXIOS GET OIDC Profile retrieved successfully', profile);
window.localStorage.peer_name = profile.name;
}
} catch (error) {
console.error('AXIOS OIDC Error fetching profile', error.message || error);
}
return window.localStorage.peer_name || '';
}
/**
* set your name for the conference
*/
async function whoAreYou() {
console.log('11. Who are you?');
document.body.style.background = 'var(--body-bg)';
if (myPeerName) {
elemDisplay(loadingDiv, false);
myPeerName = filterXSS(myPeerName);
console.log(`11.1 Check if ${myPeerName} exist in the room`, roomId);
if (await checkUserName()) {
if (!myToken) return userNameAlreadyInRoom(); // #209 Hack...
}
checkPeerAudioVideo();
whoAreYouJoin();
playSound('addPeer');
return;
}
playSound('newMessage');
// init buttons click events
initVideoBtn.onclick = async (e) => {
await handleVideo(e, true);
};
initAudioBtn.onclick = (e) => {
handleAudio(e, true);
};
initVideoMirrorBtn.onclick = (e) => {
toggleInitVideoMirror();
};
initUsernameEmojiButton.onclick = (e) => {
getId('usernameInput').value = '';
toggleUsernameEmoji();
};
await loadLocalStorage();
if (!useVideo || !buttons.main.showVideoBtn) {
useVideo = false;
elemDisplay(document.getElementById('initVideo'), false);
elemDisplay(document.getElementById('initVideoBtn'), false);
elemDisplay(document.getElementById('initVideoMirrorBtn'), false);
elemDisplay(document.getElementById('initVideoSelect'), false);
elemDisplay(document.getElementById('tabVideoBtn'), false);
}
if (!useAudio || !buttons.main.showAudioBtn) {
//useAudio = false;
elemDisplay(document.getElementById('initAudioBtn'), false);
elemDisplay(document.getElementById('initMicrophoneSelect'), false);
elemDisplay(document.getElementById('initSpeakerSelect'), false);
elemDisplay(document.getElementById('tabAudioBtn'), false);
}
if (!buttons.main.showScreenBtn) {
elemDisplay(document.getElementById('initScreenShareBtn'), false);
}
initUser.classList.toggle('hidden');
initVideoContainerShow(myVideoStatus);
window.localStorage.peer_name = await getUserName();
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
title: brand.app?.name || 'MiroTalk P2P',
position: 'center',
input: 'text',
inputPlaceholder: 'Enter your email or name',
inputAttributes: { maxlength: 254, id: 'usernameInput' },
inputValue: window.localStorage.peer_name ? window.localStorage.peer_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: () => {
elemDisplay(loadingDiv, false);
},
inputValidator: async (value) => {
if (!value) return 'Please enter your email or name';
// Long email or name
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
if ((isEmail && value.length > 254) || (!isEmail && value.length > 32)) {
return isEmail ? 'Email must be max 254 char' : 'Name must be max 32 char';
}
// prevent xss execution itself
myPeerName = filterXSS(value);
// prevent XSS injection to remote peer
if (isHtml(myPeerName)) {
myPeerName = '';
return 'Invalid name!';
}
// check if peer name is already in use in the room
if (await checkUserName()) {
return 'Username is already in use!';
} else {
// Hide username emoji
if (!usernameEmoji.classList.contains('hidden')) {
usernameEmoji.classList.add('hidden');
}
window.localStorage.peer_name = myPeerName;
whoAreYouJoin();
}
},
}).then(() => {
playSound('addPeer');
});
// select video - audio
initVideoSelect.onchange = async () => {
await changeInitCamera(initVideoSelect.value);
await handleLocalCameraMirror();
videoSelect.selectedIndex = initVideoSelect.selectedIndex;
refreshLsDevices();
};
initMicrophoneSelect.onchange = async () => {
await changeLocalMicrophone(initMicrophoneSelect.value);
audioInputSelect.selectedIndex = initMicrophoneSelect.selectedIndex;
refreshLsDevices();
};
initSpeakerSelect.onchange = async () => {
await changeAudioDestination();
audioOutputSelect.selectedIndex = initSpeakerSelect.selectedIndex;
refreshLsDevices();
};
// init video -audio buttons
if (!useVideo) {
initVideoBtn.className = className.videoOff;
setMyVideoStatus(useVideo);
}
if (!useAudio) {
initAudioBtn.className = className.audioOff;
setMyAudioStatus(useAudio);
}
setTippy(initAudioBtn, 'Stop the audio', 'top');
setTippy(initVideoBtn, 'Stop the video', 'top');
}
/**
* Refresh all LS devices
*/
async function refreshLsDevices() {
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value);
lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, audioOutputSelect.selectedIndex, audioOutputSelect.value);
}
/**
* Check if UserName already exist in the room
* @param {string} peer_name
* @returns boolean
*/
async function checkUserName(peer_name = null) {
return signalingSocket
.request('data', {
room_id: roomId,
peer_id: myPeerId,
peer_name: peer_name ? peer_name : myPeerName,
method: 'checkPeerName',
params: {},
})
.then((response) => response);
}
/**
* Username already in the room
*/
function userNameAlreadyInRoom() {
signalingSocket.disconnect();
playSound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
imageUrl: images.forbidden,
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('/');
}
});
}
/**
* Load settings from Local Storage
*/
async function loadLocalStorage() {
const localStorageDevices = lS.getLocalStorageDevices();
console.log('12. Get Local Storage Devices before', localStorageDevices);
if (localStorageDevices) {
//
const initMicrophoneExist = selectOptionByValueExist(initMicrophoneSelect, localStorageDevices.audio.select);
const initSpeakerExist = selectOptionByValueExist(initSpeakerSelect, localStorageDevices.speaker.select);
const initVideoExist = selectOptionByValueExist(initVideoSelect, localStorageDevices.video.select);
//
const audioInputExist = selectOptionByValueExist(audioInputSelect, localStorageDevices.audio.select);
const audioOutputExist = selectOptionByValueExist(audioOutputSelect, localStorageDevices.speaker.select);
const videoExist = selectOptionByValueExist(videoSelect, localStorageDevices.video.select);
console.log('Check for audio changes', {
previous: localStorageDevices.audio.select,
current: audioInputSelect.value,
});
if (!initMicrophoneExist || !audioInputExist) {
console.log('12.1 Audio devices seems changed, use default index 0');
initMicrophoneSelect.selectedIndex = 0;
audioInputSelect.selectedIndex = 0;
refreshLsDevices();
}
console.log('Check for speaker changes', {
previous: localStorageDevices.speaker.select,
current: audioOutputSelect.value,
});
if (!initSpeakerExist || !audioOutputExist) {
console.log('12.2 Speaker devices seems changed, use default index 0');
initSpeakerSelect.selectedIndex = 0;
audioOutputSelect.selectedIndex = 0;
refreshLsDevices();
}
console.log('Check for video changes', {
previous: localStorageDevices.video.select,
current: videoSelect.value,
});
if (!initVideoExist || !videoExist) {
console.log('12.3 Video devices seems changed, use default index 0');
initVideoSelect.selectedIndex = 0;
videoSelect.selectedIndex = 0;
refreshLsDevices();
}
//
console.log('12.4 Get Local Storage Devices after', lS.getLocalStorageDevices());
}
// Start init cam
if (useVideo && initVideoSelect.value) {
await changeInitCamera(initVideoSelect.value);
await handleLocalCameraMirror();
}
// Refresh audio
if (useAudio && audioInputSelect.value) {
await changeLocalMicrophone(audioInputSelect.value);
}
// Refresh speaker
if (audioOutputSelect.value) await changeAudioDestination();
// Check init audio/video
await checkInitConfig();
}
/**
* Use the select element to check if a specific option value exists,
* and if it does, automatically set it as the selected option.
* @param {object} selectElement
* @param {string} value
* @return boolean
*/
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;
}
/**
* Check int config from local storage
*/
async function checkInitConfig() {
const initConfig = lS.getInitConfig();
console.log('Get init config', initConfig);
if (initConfig) {
if (useAudio && !initConfig.audio) initAudioBtn.click();
if (useVideo && !initConfig.video) initVideoBtn.click();
}
}
/**
* Detects whether the camera stream is front-facing ('user') or rear-facing ('environment').
* Defaults to 'user' (front-facing) if detection fails (e.g., desktop cameras).
* @param {MediaStream} stream - The video stream from `getUserMedia`.
* @returns {string} 'user' (front) or 'environment' (rear).
*/
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?.() || {};
// Priority: settings.facingMode (actual) → capabilities.facingMode (possible) → default 'user'
const facingMode = settings.facingMode || capabilities.facingMode?.[0] || 'user';
return facingMode === 'environment' ? 'environment' : 'user'; // Force valid output
}
/**
* Change init camera by device id
* @param {string} deviceId
*/
async function changeInitCamera(deviceId) {
// Stop media tracks to avoid issue on mobile
if (initStream) {
await stopTracks(initStream);
}
if (localVideoMediaStream) {
await stopVideoTracks(localVideoMediaStream);
}
// Get video constraints
const videoConstraints = await getVideoConstraints('default');
videoConstraints['deviceId'] = { exact: deviceId };
await navigator.mediaDevices
.getUserMedia({ video: videoConstraints })
.then((camStream) => {
updateInitLocalVideoMediaStream(camStream);
})
.catch(async (err) => {
console.error('Error accessing init video device', err);
console.warn('Fallback to default constraints');
try {
const camStream = await navigator.mediaDevices.getUserMedia({
video: {
deviceId: {
exact: deviceId, // Specify the exact device ID you want to access
},
},
}); // Fallback to default constraints
updateInitLocalVideoMediaStream(camStream);
} catch (fallbackErr) {
console.error('Error accessing init video device with default constraints', fallbackErr);
reloadBrowser(err);
}
});
/**
* Update Init/Local Video Stream
* @param {MediaStream} camStream
*/
function updateInitLocalVideoMediaStream(camStream) {
if (camStream) {
// Detect camera
camera = detectCameraFacingMode(camStream);
console.log('Detect Camera facing mode', camera);
// We going to update init video stream
initVideo.srcObject = camStream;
initStream = camStream;
console.log('Success attached init video stream', initStream.getVideoTracks()[0].getSettings());
// We going to update also the local video stream
myVideo.srcObject = camStream;
localVideoMediaStream = camStream;
console.log('Success attached local video stream', localVideoMediaStream.getVideoTracks()[0].getSettings());
}
}
/**
* Something going wrong
* @param {object} err
*/
function reloadBrowser(err) {
console.error('[Error] changeInitCamera', err);
userLog('error', 'Error while swapping init camera' + err);
initVideoSelect.selectedIndex = 0;
videoSelect.selectedIndex = 0;
refreshLsDevices();
// Refresh page...
setTimeout(function () {
location.reload();
}, 3000);
}
}
/**
* Change local camera by device id
* @param {string} deviceId
*/
async function changeLocalCamera(deviceId) {
if (localVideoMediaStream) {
await stopVideoTracks(localVideoMediaStream);
}
// Get video constraints
const videoConstraints = await getVideoConstraints(videoQualitySelect.value ? videoQualitySelect.value : 'default');
videoConstraints['deviceId'] = { exact: deviceId };
console.log('videoConstraints', videoConstraints);
await navigator.mediaDevices
.getUserMedia({ video: videoConstraints })
.then(async (camStream) => {
await updateLocalVideoMediaStream(camStream);
})
.catch(async (err) => {
console.error('Error accessing local video device:', err);
console.warn('Fallback to default constraints');
try {
const camStream = await navigator.mediaDevices.getUserMedia({
video: {
deviceId: {
exact: deviceId, // Specify the exact device ID you want to access
},
},
});
await updateLocalVideoMediaStream(camStream);
} catch (fallbackErr) {
console.error('Error accessing init video device with default constraints', fallbackErr);
printError(err);
}
});
/**
* Update Local Video Media Stream
* @param {MediaStream} camStream
*/
async function updateLocalVideoMediaStream(camStream) {
if (camStream) {
camera = detectCameraFacingMode(camStream);
console.log('Detect Camera facing mode', camera);
myVideo.srcObject = camStream;
localVideoMediaStream = camStream;
logStreamSettingsInfo('Success attached local video stream', camStream);
await refreshMyStreamToPeers(camStream);
setLocalMaxFps(videoMaxFrameRate);
}
}
/**
* SOmething going wrong
* @param {object} err
*/
function printError(err) {
console.error('[Error] changeLocalCamera', err);
userLog('error', 'Error while swapping local camera ' + err);
}
}
/**
* Change local microphone by device id
* @param {string} deviceId
*/
async function changeLocalMicrophone(deviceId) {
if (localAudioMediaStream) {
await stopAudioTracks(localAudioMediaStream);
}
// Get audio constraints
const audioConstraints = getAudioConstraints(deviceId);
console.log('audioConstraints', audioConstraints);
await navigator.mediaDevices
.getUserMedia(audioConstraints)
.then(async (micStream) => {
myAudio.srcObject = micStream;
localAudioMediaStream = micStream;
logStreamSettingsInfo('Success attached local microphone stream', micStream);
getMicrophoneVolumeIndicator(micStream);
lsSettings.mic_noise_suppression
? await restartNoiseSuppression()
: await refreshMyStreamToPeers(micStream, true);
})
.catch((err) => {
console.error('[Error] changeLocalMicrophone', err);
userLog('error', 'Error while swapping local microphone' + err);
});
}
/**
* Check peer audio and video &audio=1&video=1
* 1/true = enabled / 0/false = disabled
*/
function checkPeerAudioVideo() {
let qs = new URLSearchParams(window.location.search);
let audio = filterXSS(qs.get('audio'));
let video = filterXSS(qs.get('video'));
if (audio) {
audio = audio.toLowerCase();
let queryPeerAudio = useAudio ? audio === '1' || audio === 'true' : false;
if (queryPeerAudio != null) handleAudio(audioBtn, false, queryPeerAudio);
//elemDisplay(tabAudioBtn, queryPeerAudio);
console.log('Direct join', { audio: queryPeerAudio });
}
if (video) {
video = video.toLowerCase();
let queryPeerVideo = useVideo ? video === '1' || video === 'true' : false;
if (queryPeerVideo != null) handleVideo(videoBtn, false, queryPeerVideo);
//elemDisplay(tabVideoBtn, queryPeerVideo);
console.log('Direct join', { video: queryPeerVideo });
}
}
/**
* Enable RNNoise audio processing for noise suppression
*/
async function enableNoiseSuppression() {
if (!localAudioMediaStream) {
userLog('error', 'No local audio stream available for noise suppression.');
return;
}
if (!noiseProcessor) noiseProcessor = new RNNoiseProcessor();
const processedStream = await noiseProcessor.startProcessing(localAudioMediaStream);
noiseProcessor.toggleNoiseSuppression();
localAudioMediaStream = processedStream;
await refreshMyStreamToPeers(localAudioMediaStream, true);
}
/**
* Disable RNNoise audio processing for noise suppression
*/
async function disableNoiseSuppression() {
if (noiseProcessor) {
localAudioMediaStream = noiseProcessor.mediaStream || localAudioMediaStream;
await refreshMyStreamToPeers(localAudioMediaStream, true);
noiseProcessor.toggleNoiseSuppression();
await noiseProcessor.stopProcessing();
noiseProcessor = null;
} else {
await refreshMyStreamToPeers(localAudioMediaStream, true);
}
}
/**
* Restart noise suppression (e.g. after changing mic)
*/
async function restartNoiseSuppression() {
if (!lsSettings.mic_noise_suppression) return;
await disableNoiseSuppression();
await enableNoiseSuppression();
}
/**
* Room and Peer name are ok Join Channel
*/
async function whoAreYouJoin() {
myVideoParagraph.innerText = myPeerName + ' (me)';
setPeerAvatarImgName('myVideoAvatarImage', myPeerName, myPeerAvatar);
setPeerAvatarImgName('myProfileAvatar', myPeerName, myPeerAvatar);
setPeerChatAvatarImgName('right', myPeerName, myPeerAvatar);
joinToChannel();
handleHideMe(isHideMeActive);
loadGeo();
}
/**
* join to channel and send some peer info
*/
async function joinToChannel() {
console.log('12. join to channel', roomId);
sendToServer('join', {
join_data_time: getDataTimeString(),
channel: roomId,
channel_password: thisRoomPassword,
peer_info: peerInfo,
peer_uuid: myPeerUUID,
peer_name: myPeerName,
peer_avatar: myPeerAvatar,
peer_token: myToken,
peer_video: useVideo,
peer_audio: useAudio,
peer_video_status: myVideoStatus,
peer_audio_status: myAudioStatus,
peer_screen_status: myScreenStatus,
peer_hand_status: myHandStatus,
peer_rec_status: isStreamRecording,
peer_privacy_status: isVideoPrivacyActive,
userAgent: userAgent,
});
handleBodyOnMouseMove(); // show/hide buttonsBar, bottomButtons ...
makeRoomPopupQR();
}
/**
* When we join a group, our signaling server will send out 'addPeer' events to each pair of users in the group (creating a fully-connected graph of users,
* ie if there are 6 people in the channel you will connect directly to the other 5, so there will be a total of 15 connections in the network).
* @param {object} config data
*/
async function handleAddPeer(config) {
//console.log("addPeer", JSON.stringify(config));
const { peer_id, should_create_offer, iceServers, peers } = config;
const peer_name = peers[peer_id]['peer_name'];
const peer_video = peers[peer_id]['peer_video'];
if (peer_id in peerConnections) {
// This could happen if the user joins multiple channels where the other peer is also in.
return console.log('Already connected to peer', peer_id);
}
console.log('iceServers', iceServers[0]);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection
const peerConnection = new RTCPeerConnection({ iceServers: iceServers });
peerConnections[peer_id] = peerConnection;
allPeers = peers;
console.log('[RTCPeerConnection] - PEER_ID', peer_id); // the connected peer_id
console.log('[RTCPeerConnection] - PEER-CONNECTIONS', peerConnections); // all peers connections in the room expect myself
console.log('[RTCPeerConnection] - PEERS', peers); // all peers in the room
// As P2P check who I am connected with
let connectedPeersName = [];
for (const id in peerConnections) {
connectedPeersName.push(peers[id]['peer_name']);
}
console.log('[RTCPeerConnection] - CONNECTED TO PEERS', JSON.stringify(connectedPeersName));
// userLog('info', 'Connected to: ' + JSON.stringify(connectedPeersName));
await handlePeersConnectionStatus(peer_id);
await msgerAddPeers(peers);
await handleOnIceCandidate(peer_id);
await handleRTCDataChannels(peer_id);
await handleOnTrack(peer_id, peers);
await handleAddTracks(peer_id);
if (!peer_video && !needToCreateOffer) {
needToCreateOffer = true;
}
if (should_create_offer) {
await handleRtcOffer(peer_id);
console.log('[RTCPeerConnection] - SHOULD CREATE OFFER', {
peer_id: peer_id,
peer_name: peer_name,
});
}
if (!peer_video) {
await loadRemoteMediaStream(new MediaStream(), peers, peer_id, 'video');
}
await wbUpdate();
playSound('addPeer');
}
/**
* Handle peers connection state
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState
* @param {string} peer_id socket.id
*/
async function handlePeersConnectionStatus(peer_id) {
peerConnections[peer_id].onconnectionstatechange = function (event) {
const connectionStatus = event.currentTarget.connectionState;
const signalingState = event.currentTarget.signalingState;
const peerName = allPeers[peer_id]['peer_name'];
console.log('[RTCPeerConnection] - CONNECTION', {
peer_id: peer_id,
peer_name: peerName,
connectionStatus: connectionStatus,
signalingState: signalingState,
});
};
}
/**
* Handle ICE candidate
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onicecandidate
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/icecandidateerror_event
* @param {string} peer_id socket.id
*/
async function handleOnIceCandidate(peer_id) {
peerConnections[peer_id].onicecandidate = (event) => {
if (!event.candidate || !event.candidate.candidate) return;
const { type, candidate, address, sdpMLineIndex } = event.candidate;
//console.log('[ICE-CANDIDATE] ---->', { type, address, candidate });
sendToServer('relayICE', {
peer_id,
ice_candidate: {
sdpMLineIndex,
candidate,
},
});
// Get Ice address
const ipRegex = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
let addressInfo = candidate.match(ipRegex);
if (!addressInfo && address) addressInfo = [address];
// IP
if (addressInfo) {
networkIP.innerText = addressInfo;
}
// Display network information based on candidate type
switch (type) {
case 'host':
networkHost.innerText = '🟢';
break;
case 'srflx':
networkStun.innerText = '🟢';
break;
case 'relay':
networkTurn.innerText = '🟢';
break;
default:
console.warn(`[ICE candidate] unknown type: ${type}`, candidate);
break;
}
};
// handle ICE candidate errors
peerConnections[peer_id].onicecandidateerror = (event) => {
const { url, errorText } = event;
console.warn('[ICE candidate] error', { url, error: errorText });
if (url.startsWith('host:')) networkHost.innerText = '🔴';
if (url.startsWith('stun:')) networkStun.innerText = '🔴';
if (url.startsWith('turn:')) networkTurn.innerText = '🔴';
//msgPopup('warning', `${url}: ${errorText}`, 'top-end', 6000);
};
}
/**
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
* @param {string} peer_id socket.id
* @param {object} peers all peers info connected to the same room
*/
async function handleOnTrack(peer_id, peers) {
console.log('[ON TRACK] - peer_id', { peer_id: peer_id });
peerConnections[peer_id].ontrack = (event) => {
const remoteVideoStream = getId(`${peer_id}___video`);
const remoteAudioStream = getId(`${peer_id}___audio`);
const remoteAvatarImage = getId(`${peer_id}_avatar`);
const peerInfo = peers[peer_id];
const { peer_name } = peerInfo;
const { kind } = event.track;
console.log('[ON TRACK] - info', { peer_id, peer_name, kind });
if (event.streams && event.streams[0]) {
console.log('[ON TRACK] - peers', peers);
switch (kind) {
case 'video':
remoteVideoStream
? attachMediaStream(remoteVideoStream, event.streams[0])
: loadRemoteMediaStream(event.streams[0], peers, peer_id, kind);
break;
case 'audio':
remoteAudioStream && isAudioTrack
? attachMediaStream(remoteAudioStream, event.streams[0])
: loadRemoteMediaStream(event.streams[0], peers, peer_id, kind);
break;
default:
break;
}
} else {
console.log('[ON TRACK] - SCREEN SHARING', { peer_id, peer_name, kind });
// Create a new screen share video stream from track video (refreshMyStreamToPeers)
const inboundStream = new MediaStream([event.track]);
attachMediaStream(remoteVideoStream, inboundStream);
elemDisplay(remoteAvatarImage, false);
elemDisplay(remoteVideoStream, true, 'block');
}
};
}
/**
* Add my localVideoMediaStream and localAudioMediaStream Tracks to connected peer
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack
* @param {string} peer_id socket.id
*/
async function handleAddTracks(peer_id) {
const peer_name = allPeers[peer_id]['peer_name'];
const videoTrack = localVideoMediaStream && localVideoMediaStream.getVideoTracks()[0];
const audioTrack = localAudioMediaStream && localAudioMediaStream.getAudioTracks()[0];
console.log('handleAddTracks', {
videoTrack: videoTrack,
audioTrack: audioTrack,
});
if (videoTrack) {
console.log('[ADD VIDEO TRACK] to Peer Name [' + peer_name + ']');
await peerConnections[peer_id].addTrack(videoTrack, localVideoMediaStream);
}
if (audioTrack) {
console.log('[ADD AUDIO TRACK] to Peer Name [' + peer_name + ']');
await peerConnections[peer_id].addTrack(audioTrack, localAudioMediaStream);
}
}
/**
* Secure RTC Data Channel
* https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ondatachannel
* https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/onmessage
* @param {string} peer_id socket.id
*/
async function handleRTCDataChannels(peer_id) {
peerConnections[peer_id].ondatachannel = (event) => {
console.log('handleRTCDataChannels ' + peer_id, event);
event.channel.onmessage = (msg) => {
switch (event.channel.label) {
case 'mirotalk_chat_channel':
try {
const dataMessage = JSON.parse(msg.data);
switch (dataMessage.type) {
case 'chat':
handleDataChannelChat(dataMessage);
break;
case 'speech':
handleDataChannelSpeechTranscript(dataMessage);
break;
case 'micVolume':
handlePeerVolume(dataMessage);
break;
default:
break;
}
} catch (err) {
console.error('mirotalk_chat_channel', err);
}
break;
case 'mirotalk_file_sharing_channel':
try {
const dataFile = msg.data;
if (dataFile instanceof ArrayBuffer && dataFile.byteLength != 0) {
handleDataChannelFileSharing(dataFile);
} else {
// Work around for Firefox Bug: even if set dc.binaryType to arraybuffer it sends Blob?
if (dataFile instanceof Blob && dataFile.size != 0) {
blobToArrayBuffer(dataFile)
.then((arrayBuffer) => {
handleDataChannelFileSharing(arrayBuffer);
})
.catch((error) => {
console.error('mirotalk_file_sharing_channel', error);
});
}
}
} catch (err) {
console.error('mirotalk_file_sharing_channel', err);
}
break;
default:
break;
}
};
};
createChatDataChannel(peer_id);
createFileSharingDataChannel(peer_id);
}
/**
* Convert Blob to ArrayBuffer
* @param {object} blob
* @returns arrayBuffer
*/
function blobToArrayBuffer(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
resolve(arrayBuffer);
};
reader.onerror = () => {
reject(new Error('Error reading Blob as ArrayBuffer'));
};
reader.readAsArrayBuffer(blob);
});
}
/**
* Only one side of the peer connection should create the offer, the signaling server picks one to be the offerer.
* The other user will get a 'sessionDescription' event and will create an offer, then send back an answer 'sessionDescription' to us
* @param {string} peer_id socket.id
*/
async function handleRtcOffer(peer_id) {
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onnegotiationneeded
peerConnections[peer_id].onnegotiationneeded = () => {
console.log('Creating RTC offer to ' + allPeers[peer_id]['peer_name']);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer
peerConnections[peer_id]
.createOffer()
.then((local_description) => {
console.log('Local offer description is', local_description);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription
peerConnections[peer_id]
.setLocalDescription(local_description)
.then(() => {
sendToServer('relaySDP', {
peer_id: peer_id,
session_description: local_description,
});
console.log('Offer setLocalDescription done!');
})
.catch((err) => {
console.error('[Error] offer setLocalDescription', err);
userLog('error', 'Offer setLocalDescription failed ' + err);
});
})
.catch((err) => {
console.error('[Error] sending offer', err);
});
};
}
/**
* Peers exchange session descriptions which contains information about their audio / video settings and that sort of stuff. First
* the 'offerer' sends a description to the 'answerer' (with type "offer"), then the answerer sends one back (with type "answer").
* @param {object} config data
*/
function handleSessionDescription(config) {
console.log('Remote Session Description', config);
const { peer_id, session_description } = config;
// https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription
const remote_description = new RTCSessionDescription(session_description);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setRemoteDescription
peerConnections[peer_id]
.setRemoteDescription(remote_description)
.then(() => {
console.log('setRemoteDescription done!');
if (session_description.type == 'offer') {
console.log('Creating answer');
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer
peerConnections[peer_id]
.createAnswer()
.then((local_description) => {
console.log('Answer description is: ', local_description);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription
peerConnections[peer_id]
.setLocalDescription(local_description)
.then(() => {
sendToServer('relaySDP', {
peer_id: peer_id,
session_description: local_description,
});
console.log('Answer setLocalDescription done!');
// https://github.com/miroslavpejic85/mirotalk/issues/110
if (needToCreateOffer) {
needToCreateOffer = false;
handleRtcOffer(peer_id);
console.log('[RTCSessionDescription] - NEED TO CREATE OFFER', {
peer_id: peer_id,
});
}
})
.catch((err) => {
console.error('[Error] answer setLocalDescription', err);
userLog('error', 'Answer setLocalDescription failed ' + err);
});
})
.catch((err) => {
console.error('[Error] creating answer', err);
});
} // end [if type offer]
})
.catch((err) => {
console.error('[Error] setRemoteDescription', err);
});
}
/**
* The offerer will send a number of ICE Candidate blobs to the answerer so they
* can begin trying to find the best path to one another on the net.
* @param {object} config data
*/
function handleIceCandidate(config) {
const { peer_id, ice_candidate } = config;
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
peerConnections[peer_id].addIceCandidate(new RTCIceCandidate(ice_candidate)).catch((err) => {
console.error('[Error] addIceCandidate', err);
});
}
/**
* Disconnected from Signaling Server.
* Tear down all of our peer connections and remove all the media divs.
* @param {object} reason of disconnection
*/
function handleDisconnect(reason) {
console.log('Disconnected from signaling server', { reason: reason });
checkRecording();
for (const peer_id in peerConnections) {
const peerVideoId = peer_id + '___video';
const peerAudioId = peer_id + '___audio';
const peerVideo = getId(peerVideoId);
if (peerVideo) {
// Peer video in focus mode
if (peerVideo.hasAttribute('focus-mode')) {
const remoteVideoFocusBtn = getId(peer_id + '_focusMode');
if (remoteVideoFocusBtn) {
remoteVideoFocusBtn.click();
}
}
}
if (peerVideoMediaElements[peerVideoId] && peerVideoMediaElements[peerVideoId].parentNode) {
peerVideoMediaElements[peerVideoId].parentNode.removeChild(peerVideoMediaElements[peerVideoId]);
}
if (peerAudioMediaElements[peerAudioId] && peerAudioMediaElements[peerAudioId].parentNode) {
peerAudioMediaElements[peerAudioId].parentNode.removeChild(peerAudioMediaElements[peerAudioId]);
}
peerConnections[peer_id].close();
msgerRemovePeer(peer_id);
removeVideoPinMediaContainer(peer_id);
}
adaptAspectRatio();
chatDataChannels = {};
fileDataChannels = {};
peerConnections = {};
peerVideoMediaElements = {};
peerAudioMediaElements = {};
isPeerReconnected = true;
}
/**
* When a user leaves a channel (or is disconnected from the signaling server) everyone will recieve a 'removePeer' message
* telling them to trash the media channels they have open for those that peer. If it was this client that left a channel,
* they'll also receive the removePeers. If this client was disconnected, they wont receive removePeers, but rather the
* signaling_socket.on('disconnect') code will kick in and tear down all the peer sessions.
* @param {object} config data
*/
function handleRemovePeer(config) {
console.log('Signaling server said to remove peer:', config);
const { peer_id } = config;
const peerVideoId = peer_id + '___video';
const peerAudioId = peer_id + '___audio';
if (peerVideoId in peerVideoMediaElements) {
const peerVideo = getId(peerVideoId);
if (peerVideo) {
// Peer video in focus mode
if (peerVideo.hasAttribute('focus-mode')) {
const remoteVideoFocusBtn = getId(peer_id + '_focusMode');
if (remoteVideoFocusBtn) {
remoteVideoFocusBtn.click();
}
}
}
peerVideoMediaElements[peerVideoId].parentNode.removeChild(peerVideoMediaElements[peerVideoId]);
adaptAspectRatio();
}
if (peerAudioId in peerAudioMediaElements) {
peerAudioMediaElements[peerAudioId].parentNode.removeChild(peerAudioMediaElements[peerAudioId]);
}
if (peer_id in peerConnections) peerConnections[peer_id].close();
msgerRemovePeer(peer_id);
removeVideoPinMediaContainer(peer_id);
delete chatDataChannels[peer_id];
delete fileDataChannels[peer_id];
delete peerConnections[peer_id];
delete peerVideoMediaElements[peerVideoId];
delete peerAudioMediaElements[peerAudioId];
delete allPeers[peer_id];
playSound('removePeer');
console.log('ALL PEERS', allPeers);
}
/**
* Set custom theme
*/
function setCustomTheme() {
const color = themeCustom.color;
swBg = `radial-gradient(${color}, ${color})`;
setSP('--body-bg', `radial-gradient(${color}, ${color})`);
setSP('--msger-bg', `radial-gradient(${color}, ${color})`);
setSP('--msger-private-bg', `radial-gradient(${color}, ${color})`);
setSP('--wb-bg', `radial-gradient(${color}, ${color})`);
setSP('--elem-border-color', '0.5px solid rgb(255 255 255 / 32%)');
setSP('--navbar-bg', 'rgba(0, 0, 0, 0.2)');
setSP('--select-bg', `${color}`);
setSP('--tab-btn-active', `${color}`);
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.2)');
setSP('--left-msg-bg', '#252d31');
setSP('--right-msg-bg', `${color}`);
setSP('--private-msg-bg', '#6b1226');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', `${color}`);
document.body.style.background = `radial-gradient(${color}, ${color})`;
}
/**
* Set mirotalk theme | dark | grey | ...
*/
function setTheme() {
if (themeCustom.keep) return setCustomTheme();
mirotalkTheme.selectedIndex = lsSettings.theme;
const theme = mirotalkTheme.value;
switch (theme) {
case 'dark':
// dark theme
swBg = 'radial-gradient(#393939, #000000)';
setSP('--body-bg', 'radial-gradient(#393939, #000000)');
setSP('--msger-bg', 'radial-gradient(#393939, #000000)');
setSP('--msger-private-bg', 'radial-gradient(#393939, #000000)');
setSP('--wb-bg', 'radial-gradient(#393939, #000000)');
setSP('--elem-border-color', 'none');
setSP('--navbar-bg', 'rgba(28, 28, 28, 0.8)');
setSP('--select-bg', '#3a3a3a');
setSP('--tab-btn-active', '#4f4f4f');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.4)');
setSP('--left-msg-bg', '#353535');
setSP('--right-msg-bg', '#4a4a4a');
setSP('--private-msg-bg', '#2a2a2a');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', 'rgba(0, 0, 0, 0.7)');
setSP('--dd-color', '#FFFFFF');
document.body.style.background = 'radial-gradient(#393939, #000000)';
mirotalkTheme.selectedIndex = 0;
break;
case 'grey':
// grey theme
swBg = 'radial-gradient(#4f4f4f, #1c1c1c)';
setSP('--body-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--msger-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--wb-bg', 'radial-gradient(#5f5f5f, #2c2c2c)');
setSP('--elem-border-color', 'none');
setSP('--navbar-bg', 'rgba(28, 28, 28, 0.8)');
setSP('--select-bg', '#3a3a3a');
setSP('--tab-btn-active', '#4f4f4f');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.4)');
setSP('--msger-private-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--left-msg-bg', '#353535');
setSP('--right-msg-bg', '#4a4a4a');
setSP('--private-msg-bg', '#616161');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', 'rgba(0, 0, 0, 0.7)');
setSP('--dd-color', '#FFFFFF');
document.body.style.background = 'radial-gradient(#4f4f4f, #1c1c1c)';
mirotalkTheme.selectedIndex = 1;
break;
case 'green':
// green theme
swBg = 'radial-gradient(#004d40, #001f1c)';
setSP('--body-bg', 'radial-gradient(#004d40, #001f1c)');
setSP('--msger-bg', 'radial-gradient(#004d40, #001f1c)');
setSP('--wb-bg', 'radial-gradient(#004d40, #001f1c)');
setSP('--elem-border-color', 'none');
setSP('--navbar-bg', 'rgba(0, 31, 28, 0.8)');
setSP('--select-bg', '#002e2b');
setSP('--tab-btn-active', '#004d40');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.4)');
setSP('--msger-private-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--left-msg-bg', '#004d40');
setSP('--right-msg-bg', '#00312c');
setSP('--private-msg-bg', '#004a47');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', 'rgba(0, 42, 34, 0.7)');
setSP('--dd-color', '#00FF00');
document.body.style.background = 'radial-gradient(#004d40, #001f1c)';
mirotalkTheme.selectedIndex = 2;
break;
case 'blue':
// blue theme
swBg = 'radial-gradient(#1a237e, #0d1b34)';
setSP('--body-bg', 'radial-gradient(#1a237e, #0d1b34)');
setSP('--msger-bg', 'radial-gradient(#1a237e, #0d1b34)');
setSP('--wb-bg', 'radial-gradient(#1a237e, #0d1b34)');
setSP('--elem-border-color', 'none');
setSP('--navbar-bg', 'rgba(13, 27, 52, 0.8)');
setSP('--select-bg', '#0d1b34');
setSP('--tab-btn-active', '#1a237e');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.4)');
setSP('--msger-private-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--left-msg-bg', '#1a237e');
setSP('--right-msg-bg', '#0d1b34');
setSP('--private-msg-bg', '#1a237e');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', 'rgba(0, 39, 77, 0.7)');
setSP('--dd-color', '#1E90FF');
document.body.style.background = 'radial-gradient(#1a237e, #0d1b34)';
mirotalkTheme.selectedIndex = 3;
break;
case 'red':
// red theme
swBg = 'radial-gradient(#8B0000, #320000)';
setSP('--body-bg', 'radial-gradient(#8B0000, #320000)');
setSP('--msger-bg', 'radial-gradient(#8B0000, #320000)');
setSP('--wb-bg', 'radial-gradient(#8B0000, #320000)');
setSP('--navbar-bg', 'rgba(50, 0, 0, 0.8)');
setSP('--select-bg', '#320000');
setSP('--tab-btn-active', '#8B0000');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.4)');
setSP('--msger-private-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--left-msg-bg', '#8B0000');
setSP('--right-msg-bg', '#4B0000');
setSP('--private-msg-bg', '#8B0000');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', 'rgba(42, 13, 13, 0.7)');
setSP('--dd-color', '#FF4500');
document.body.style.background = 'radial-gradient(#8B0000, #320000)';
mirotalkTheme.selectedIndex = 4;
break;
case 'purple':
// purple theme
swBg = 'radial-gradient(#4B0082, #2C003E)';
setSP('--body-bg', 'radial-gradient(#4B0082, #2C003E)');
setSP('--msger-bg', 'radial-gradient(#4B0082, #2C003E)');
setSP('--wb-bg', 'radial-gradient(#4B0082, #2C003E)');
setSP('--elem-border-color', 'none');
setSP('--navbar-bg', 'rgba(44, 0, 62, 0.8)');
setSP('--select-bg', '#2C003E');
setSP('--tab-btn-active', '#4B0082');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.4)');
setSP('--msger-private-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--left-msg-bg', '#4B0082');
setSP('--right-msg-bg', '#2C003E');
setSP('--private-msg-bg', '#4B0082');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', 'rgba(42, 0, 29, 0.7)');
setSP('--dd-color', '#BF00FF');
document.body.style.background = 'radial-gradient(#4B0082, #2C003E)';
mirotalkTheme.selectedIndex = 5;
break;
case 'orange':
// orange theme
swBg = 'radial-gradient(#FF8C00, #4B1C00)';
setSP('--body-bg', 'radial-gradient(#FF8C00, #4B1C00)');
setSP('--msger-bg', 'radial-gradient(#FF8C00, #4B1C00)');
setSP('--wb-bg', 'radial-gradient(#FF8C00, #4B1C00)');
setSP('--elem-border-color', 'none');
setSP('--navbar-bg', 'rgba(75, 28, 0, 0.8)');
setSP('--select-bg', '#4B1C00');
setSP('--tab-btn-active', '#FF8C00');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.4)');
setSP('--msger-private-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--left-msg-bg', '#FF8C00');
setSP('--right-msg-bg', '#4B1C00');
setSP('--private-msg-bg', '#FF8C00');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', 'rgba(61, 26, 0, 0.7)');
setSP('--dd-color', '#FFA500');
document.body.style.background = 'radial-gradient(#FF8C00, #4B1C00)';
mirotalkTheme.selectedIndex = 6;
break;
case 'yellow':
// yellow theme
swBg = 'radial-gradient(#FFD700, #3B3B00)';
setSP('--body-bg', 'radial-gradient(#FFD700, #3B3B00)');
setSP('--msger-bg', 'radial-gradient(#FFD700, #3B3B00)');
setSP('--wb-bg', 'radial-gradient(#FFD700, #3B3B00)');
setSP('--elem-border-color', 'none');
setSP('--navbar-bg', 'rgba(59, 59, 0, 0.8)');
setSP('--select-bg', '#3B3B00');
setSP('--tab-btn-active', '#FFD700');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.4)');
setSP('--msger-private-bg', 'radial-gradient(#4f4f4f, #1c1c1c)');
setSP('--left-msg-bg', '#FFD700');
setSP('--right-msg-bg', '#B8860B');
setSP('--private-msg-bg', '#FFD700');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
setSP('--btns-bg-color', 'rgba(77, 59, 0, 0.7)');
setSP('--dd-color', '#FFD700');
document.body.style.background = 'radial-gradient(#FFD700, #3B3B00)';
mirotalkTheme.selectedIndex = 7;
break;
// ...
default:
console.log('No theme found');
break;
}
//setButtonsBarPosition(mainButtonsBarPosition);
}
/**
* Set buttons bar position
* @param {string} position vertical / horizontal
*/
function setButtonsBarPosition(position) {
if (!position || isMobileDevice) return;
mainButtonsBarPosition = position;
switch (mainButtonsBarPosition) {
case 'vertical':
// buttonsBar
setSP('--btns-top', '50%');
setSP('--btns-right', '0px');
setSP('--btns-left', '15px');
setSP('--btns-margin-left', '0px');
setSP('--btns-width', '40px');
setSP('--btns-flex-direction', 'column');
// bottomButtons horizontally
setSP('--bottom-btns-top', 'auto');
setSP('--bottom-btns-left', '50%');
setSP('--bottom-btns-bottom', '0');
setSP('--bottom-btns-translate-X', '-50%');
setSP('--bottom-btns-translate-Y', '0%');
setSP('--bottom-btns-margin-bottom', '16px');
setSP('--bottom-btns-flex-direction', 'row');
break;
case 'horizontal':
// buttonsBar
setSP('--btns-top', '95%');
setSP('--btns-right', '25%');
setSP('--btns-left', '50%');
setSP('--btns-margin-left', '-260px');
setSP('--btns-width', '520px');
setSP('--btns-flex-direction', 'row');
// bottomButtons vertically
setSP('--bottom-btns-top', '50%');
setSP('--bottom-btns-left', '15px');
setSP('--bottom-btns-bottom', 'auto');
setSP('--bottom-btns-translate-X', '0%');
setSP('--bottom-btns-translate-Y', '-50%');
setSP('--bottom-btns-margin-bottom', '0');
setSP('--bottom-btns-flex-direction', 'column');
break;
default:
console.log('No position found');
break;
}
refreshMainButtonsToolTipPlacement();
}
/**
* Init to enumerate the devices
*/
async function initEnumerateDevices() {
console.log('05. init Enumerate Video and Audio Devices');
await initEnumerateVideoDevices();
await initEnumerateAudioDevices();
}
/**
* Init to enumerate the audio devices
* @returns boolean true/false
*/
async function initEnumerateAudioDevices() {
if (isEnumerateAudioDevices) return;
// allow the audio
await navigator.mediaDevices
.getUserMedia({ audio: true })
.then(async (stream) => {
await enumerateAudioDevices(stream);
useAudio = true;
})
.catch(() => {
useAudio = false;
});
}
/**
* Init to enumerate the vide devices
* @returns boolean true/false
*/
async function initEnumerateVideoDevices() {
if (isEnumerateVideoDevices) return;
// allow the video
await navigator.mediaDevices
.getUserMedia({ video: true })
.then(async (stream) => {
await enumerateVideoDevices(stream);
useVideo = true;
})
.catch(() => {
useVideo = false;
});
}
/**
* Enumerate Audio
* @param {object} stream
*/
async function enumerateAudioDevices(stream) {
console.log('06. Get Audio Devices');
await navigator.mediaDevices
.enumerateDevices()
.then((devices) =>
devices.forEach(async (device) => {
let el,
eli = null;
if ('audioinput' === device.kind) {
el = audioInputSelect;
eli = initMicrophoneSelect;
lS.DEVICES_COUNT.audio++;
} else if ('audiooutput' === device.kind) {
el = audioOutputSelect;
eli = initSpeakerSelect;
lS.DEVICES_COUNT.speaker++;
}
if (!el) return;
await addChild(device, [el, eli]);
})
)
.then(async () => {
await stopTracks(stream);
isEnumerateAudioDevices = true;
//const sinkId = 'sinkId' in HTMLMediaElement.prototype;
audioOutputSelect.disabled = !sinkId;
// Check if there is speakers
if (!sinkId || initSpeakerSelect.options.length === 0) {
elemDisplay(initSpeakerSelect, false);
elemDisplay(audioOutputDiv, false);
}
});
}
/**
* Enumerate Video
* @param {object} stream
*/
async function enumerateVideoDevices(stream) {
console.log('07. Get Video Devices');
await navigator.mediaDevices
.enumerateDevices()
.then((devices) =>
devices.forEach(async (device) => {
let el,
eli = null;
if ('videoinput' === device.kind) {
el = videoSelect;
eli = initVideoSelect;
lS.DEVICES_COUNT.video++;
}
if (!el) return;
await addChild(device, [el, eli]);
})
)
.then(async () => {
await stopTracks(stream);
isEnumerateVideoDevices = true;
});
}
/**
* Stop tracks from stream
* @param {object} stream
*/
async function stopTracks(stream) {
stream.getTracks().forEach((track) => {
track.stop();
});
}
/**
* Add child to element
* @param {object} device
* @param {object} els
*/
async function addChild(device, els) {
const { kind, deviceId, label } = device;
els.forEach((el) => {
const option = document.createElement('option');
option.value = deviceId;
switch (kind) {
case 'videoinput':
option.innerText = `📹 ` + label || `📹 camera ${el.length + 1}`;
break;
case 'audioinput':
option.innerText = `🎤 ` + label || `🎤 microphone ${el.length + 1}`;
break;
case 'audiooutput':
option.innerText = `🔈 ` + label || `🔈 speaker ${el.length + 1}`;
break;
default:
break;
}
el.appendChild(option);
});
}
/**
* Setup local video media. Ask the user for permission to use the computer's camera,
* and attach it to a <video> tag if access is granted.
* https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
*/
async function setupLocalVideoMedia() {
if (!useVideo || localVideoMediaStream) {
return;
}
console.log('Requesting access to video inputs');
const videoConstraints = useVideo ? await getVideoConstraints('default') : false;
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
await updateLocalVideoMediaStream(stream);
} catch (err) {
console.error('Error accessing video device', err);
console.warn('Fallback to default constraints');
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
await updateLocalVideoMediaStream(stream);
} catch (fallbackErr) {
console.error('Error accessing video device with default constraints', fallbackErr);
handleMediaError('video', fallbackErr);
}
}
/**
* Update Local Media Stream
* @param {MediaStream} stream
*/
async function updateLocalVideoMediaStream(stream) {
if (stream) {
localVideoMediaStream = stream;
await loadLocalMedia(stream, 'video');
console.log('Access granted to video device');
}
}
}
/**
* Setup local audio media. Ask the user for permission to use the computer's microphone,
* and attach it to an <audio> tag if access is granted.
* https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
*/
async function setupLocalAudioMedia() {
if (!useAudio || localAudioMediaStream) {
return;
}
console.log('Requesting access to audio inputs');
const audioConstraints = useAudio ? getAudioConstraints() : { audio: false };
try {
const stream = await navigator.mediaDevices.getUserMedia(audioConstraints);
if (stream) {
await loadLocalMedia(stream, 'audio');
if (useAudio) {
localAudioMediaStream = stream;
await getMicrophoneVolumeIndicator(stream);
console.log('10. Access granted to audio device');
if (lsSettings.mic_noise_suppression) {
await enableNoiseSuppression();
}
}
}
} catch (err) {
handleMediaError('audio', err);
}
}
/**
* Handle media access error.
* https://blog.addpipe.com/common-getusermedia-errors/
*
* @param {string} mediaType - 'video' or 'audio'
* @param {object} err - The error object
*/
function handleMediaError(mediaType, err) {
playSound('alert');
//
let errMessage = err;
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';
break;
case 'NotAllowedError':
case 'PermissionDeniedError':
errMessage = 'Permission denied in browser';
break;
case 'TypeError':
errMessage = 'Empty constraints object';
break;
default:
break;
}
// Print message to inform user
const $html = `
<ul style="text-align: left">
<li>Media type: ${mediaType}</li>
<li>Error name: ${err.name}</li>
<li>Error message: <p style="color: red">${errMessage}</p></li>
<li>Common: <a href="https://blog.addpipe.com/common-getusermedia-errors" target="_blank">getUserMedia errors</a></li>
</ul>
`;
msgHTML(null, images.forbidden, 'Access denied', $html, 'center', '/');
/*
it immediately stops the execution of the current function and jumps to the nearest enclosing try...catch block or,
if none exists, it interrupts the script execution and displays an error message in the console.
*/
throw new Error(
`Access denied for ${mediaType} device [${err.name}]: ${errMessage} check the common getUserMedia errors: https://blog.addpipe.com/common-getusermedia-errors/`
);
}
/**
* Load Local Media Stream obj
* @param {object} stream media stream audio - video
*/
async function loadLocalMedia(stream, kind) {
if (stream) console.log('LOAD LOCAL MEDIA STREAM TRACKS', stream.getTracks());
switch (kind) {
case 'video':
//alert('local video');
console.log('SETUP LOCAL VIDEO STREAM');
// local video elements
const myVideoWrap = document.createElement('div');
const myLocalMedia = document.createElement('video');
// html elements
const myVideoNavBar = document.createElement('div');
const mySessionTime = document.createElement('button');
const myPeerName = document.createElement('p');
const myHandStatusIcon = document.createElement('button');
const myVideoToImgBtn = document.createElement('button');
const myPrivacyBtn = document.createElement('button');
const myVideoStatusIcon = document.createElement('button');
const myAudioStatusIcon = document.createElement('button');
const myVideoFullScreenBtn = document.createElement('button');
const myVideoPinBtn = document.createElement('button');
const myVideoMirrorBtn = document.createElement('button');
const myVideoZoomInBtn = document.createElement('button');
const myVideoZoomOutBtn = document.createElement('button');
const myVideoPiPBtn = document.createElement('button');
const myVideoAvatarImage = document.createElement('img');
const myPitchMeter = document.createElement('div');
const myPitchBar = document.createElement('div');
// session time
mySessionTime.setAttribute('id', 'mySessionTime');
mySessionTime.className = 'notranslate';
mySessionTime.style.cursor = 'default';
// my peer name
myPeerName.setAttribute('id', 'myVideoParagraph');
myPeerName.className = 'videoPeerName notranslate';
// my hand status element
myHandStatusIcon.setAttribute('id', 'myHandStatusIcon');
myHandStatusIcon.className = className.handPulsate;
myHandStatusIcon.style.setProperty('color', 'rgb(0, 255, 0)');
// my privacy button
myPrivacyBtn.setAttribute('id', 'myPrivacyBtn');
myPrivacyBtn.className = className.privacy;
// my video status element
myVideoStatusIcon.setAttribute('id', 'myVideoStatusIcon');
myVideoStatusIcon.className = className.videoOn;
myVideoStatusIcon.style.cursor = 'default';
// my audio status element
myAudioStatusIcon.setAttribute('id', 'myAudioStatusIcon');
myAudioStatusIcon.className = className.audioOn;
myAudioStatusIcon.style.cursor = 'default';
// my video to image
myVideoToImgBtn.setAttribute('id', 'myVideoToImgBtn');
myVideoToImgBtn.className = className.snapShot;
// my video full screen mode
myVideoFullScreenBtn.setAttribute('id', 'myVideoFullScreenBtn');
myVideoFullScreenBtn.className = className.fullScreen;
// my video zoomIn/Out
myVideoZoomInBtn.setAttribute('id', 'myVideoZoomInBtn');
myVideoZoomInBtn.className = className.zoomIn;
myVideoZoomOutBtn.setAttribute('id', 'myVideoZoomOutBtn');
myVideoZoomOutBtn.className = className.zoomOut;
// my video Picture in Picture
myVideoPiPBtn.setAttribute('id', 'myVideoPiPBtn');
myVideoPiPBtn.className = className.pip;
// my video pin/unpin button
myVideoPinBtn.setAttribute('id', 'myVideoPinBtn');
myVideoPinBtn.className = className.pinUnpin;
// my video toggle mirror
myVideoMirrorBtn.setAttribute('id', 'myVideoMirror');
myVideoMirrorBtn.className = className.mirror;
// no mobile devices
if (!isMobileDevice) {
setTippy(mySessionTime, 'Session Time', 'bottom');
setTippy(myPeerName, 'My name', 'bottom');
setTippy(myHandStatusIcon, 'My hand is raised', 'bottom');
setTippy(myPrivacyBtn, 'Toggle video privacy', 'bottom');
setTippy(myVideoStatusIcon, 'My video is on', 'bottom');
setTippy(myAudioStatusIcon, 'My audio is on', 'bottom');
setTippy(myVideoToImgBtn, 'Take a snapshot', 'bottom');
setTippy(myVideoFullScreenBtn, 'Full screen mode', 'bottom');
setTippy(myVideoPiPBtn, 'Toggle picture in picture', 'bottom');
setTippy(myVideoZoomInBtn, 'Zoom in video', 'bottom');
setTippy(myVideoZoomOutBtn, 'Zoom out video', 'bottom');
setTippy(myVideoPinBtn, 'Toggle Pin video', 'bottom');
setTippy(myVideoMirrorBtn, 'Toggle video mirror', 'bottom');
}
// my video avatar image
myVideoAvatarImage.setAttribute('id', 'myVideoAvatarImage');
myVideoAvatarImage.className = 'videoAvatarImage'; // pulsate
// my pitch meter
myPitchMeter.setAttribute('id', 'myPitch');
myPitchBar.setAttribute('id', 'myPitchBar');
myPitchMeter.className = 'speechbar';
myPitchBar.className = 'bar';
myPitchBar.style.height = '1%';
// my video nav bar
myVideoNavBar.className = 'navbar fadein';
// attach to video nav bar
myVideoNavBar.appendChild(mySessionTime);
!isMobileDevice && myVideoNavBar.appendChild(myVideoPinBtn);
myVideoNavBar.appendChild(myVideoMirrorBtn);
if (showVideoPipBtn && buttons.local.showVideoPipBtn) myVideoNavBar.appendChild(myVideoPiPBtn);
if (buttons.local.showZoomInOutBtn) {
myVideoNavBar.appendChild(myVideoZoomInBtn);
myVideoNavBar.appendChild(myVideoZoomOutBtn);
}
isVideoFullScreenSupported && myVideoNavBar.appendChild(myVideoFullScreenBtn);
buttons.local.showSnapShotBtn && myVideoNavBar.appendChild(myVideoToImgBtn);
buttons.local.showVideoCircleBtn && myVideoNavBar.appendChild(myPrivacyBtn);
myVideoNavBar.appendChild(myVideoStatusIcon);
myVideoNavBar.appendChild(myAudioStatusIcon);
myVideoNavBar.appendChild(myHandStatusIcon);
// add my pitchBar
myPitchMeter.appendChild(myPitchBar);
// hand display none on default menad is raised == false
elemDisplay(myHandStatusIcon, false);
myLocalMedia.setAttribute('id', 'myVideo');
myLocalMedia.setAttribute('playsinline', true);
myLocalMedia.className = 'mirror';
myLocalMedia.autoplay = true;
myLocalMedia.muted = true;
myLocalMedia.volume = 0;
myLocalMedia.controls = false;
myLocalMedia.poster = images.poster;
myVideoWrap.className = 'Camera';
myVideoWrap.setAttribute('id', 'myVideoWrap');
// add elements to video wrap div
myVideoWrap.appendChild(myVideoNavBar);
myVideoWrap.appendChild(myVideoAvatarImage);
myVideoWrap.appendChild(myLocalMedia);
myVideoWrap.appendChild(myPitchMeter);
myVideoWrap.appendChild(myPeerName);
videoMediaContainer.appendChild(myVideoWrap);
elemDisplay(myVideoWrap, false);
logStreamSettingsInfo('localVideoMediaStream', stream);
attachMediaStream(myLocalMedia, stream);
adaptAspectRatio();
handleVideoToggleMirror(myLocalMedia.id, myVideoMirrorBtn.id);
isVideoFullScreenSupported && handleVideoPlayerFs(myLocalMedia.id, myVideoFullScreenBtn.id);
handleFileDragAndDrop(myLocalMedia.id, myPeerId, true);
buttons.local.showSnapShotBtn && handleVideoToImg(myLocalMedia.id, myVideoToImgBtn.id);
buttons.local.showVideoCircleBtn && handleVideoPrivacyBtn(myLocalMedia.id, myPrivacyBtn.id);
handleVideoPinUnpin(myLocalMedia.id, myVideoPinBtn.id, myVideoWrap.id, myLocalMedia.id);
if (showVideoPipBtn && buttons.local.showVideoPipBtn)
handlePictureInPicture(myVideoPiPBtn.id, myLocalMedia.id, myPeerId);
ZOOM_IN_OUT_ENABLED && handleVideoZoomInOut(myVideoZoomInBtn.id, myVideoZoomOutBtn.id, myLocalMedia.id);
refreshMyVideoStatus(stream);
if (!useVideo) {
elemDisplay(myVideoAvatarImage, true, 'block');
myVideoStatusIcon.className = className.videoOff;
videoBtn.className = className.videoOff;
if (!isMobileDevice) {
setTippy(myVideoStatusIcon, 'My video is disabled', 'bottom');
}
}
if (!useAudio) {
myAudioStatusIcon.className = className.audioOff;
audioBtn.className = className.audioOff;
if (!isMobileDevice) {
setTippy(myAudioStatusIcon, 'My audio is disabled', 'bottom');
}
}
break;
case 'audio':
//alert('local audio');
console.log('SETUP LOCAL AUDIO STREAM');
// handle remote audio elements
const localAudioWrap = document.createElement('div');
const localAudioMedia = document.createElement('audio');
localAudioMedia.id = 'myAudio';
localAudioMedia.controls = false;
localAudioMedia.autoplay = true;
localAudioMedia.muted = true;
localAudioMedia.volume = 0;
localAudioWrap.appendChild(localAudioMedia);
audioMediaContainer.appendChild(localAudioWrap);
logStreamSettingsInfo('localAudioMediaStream', stream);
attachMediaStream(localAudioMedia, stream);
refreshMyAudioStatus(stream);
break;
default:
break;
}
}
/**
* Check if screen is shared on join room
*/
function checkShareScreen() {
if (!isMobileDevice && isScreenEnabled && isScreenSharingSupported) {
playSound('newMessage');
// screenShareBtn.click(); // Chrome - Opera - Edge - Brave
// handle error: getDisplayMedia requires transient activation from a user gesture on Safari - FireFox
Swal.fire({
background: swBg,
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) {
screenShareBtn.click();
}
});
}
}
/**
* Load Remote Media Stream obj
* @param {MediaStream} stream media stream audio - video
* @param {object} peers all peers info connected to the same room
* @param {string} peer_id socket.id
*/
async function loadRemoteMediaStream(stream, peers, peer_id, kind) {
// get data from peers obj
console.log('REMOTE PEER INFO', peers[peer_id]);
const peer_name = peers[peer_id]['peer_name'];
const peer_avatar = peers[peer_id]['peer_avatar'];
const peer_audio = peers[peer_id]['peer_audio'];
const peer_video = peers[peer_id]['peer_video'];
const peer_video_status = peers[peer_id]['peer_video_status'];
const peer_audio_status = peers[peer_id]['peer_audio_status'];
const peer_screen_status = peers[peer_id]['peer_screen_status'];
const peer_hand_status = peers[peer_id]['peer_hand_status'];
const peer_rec_status = peers[peer_id]['peer_rec_status'];
const peer_privacy_status = peers[peer_id]['peer_privacy_status'];
if (stream) console.log('LOAD REMOTE MEDIA STREAM TRACKS - PeerName:[' + peer_name + ']', stream.getTracks());
switch (kind) {
case 'video':
// alert('remote video');
console.log('SETUP REMOTE VIDEO STREAM');
// handle remote video elements
const remoteVideoWrap = document.createElement('div');
const remoteMedia = document.createElement('video');
// html elements
const remoteVideoNavBar = document.createElement('div');
const remotePeerName = document.createElement('p');
const remoteHandStatusIcon = document.createElement('button');
const remoteVideoStatusIcon = document.createElement('button');
const remoteAudioStatusIcon = document.createElement('button');
const remoteVideoAudioUrlBtn = document.createElement('button');
const remoteFileShareBtn = document.createElement('button');
const remotePrivateMsgBtn = document.createElement('button');
const remoteGeoLocationBtn = document.createElement('button');
const remotePeerKickOut = document.createElement('button');
const remoteVideoToImgBtn = document.createElement('button');
const remoteVideoFullScreenBtn = document.createElement('button');
const remoteVideoPinBtn = document.createElement('button');
const remoteVideoFocusBtn = document.createElement('button');
const remoteVideoMirrorBtn = document.createElement('button');
const remoteVideoZoomInBtn = document.createElement('button');
const remoteVideoZoomOutBtn = document.createElement('button');
const remoteVideoPiPBtn = document.createElement('button');
const remoteVideoAvatarImage = document.createElement('img');
const remotePitchMeter = document.createElement('div');
const remotePitchBar = document.createElement('div');
const remoteAudioVolume = document.createElement('input');
// Expand button UI/UX
const remoteExpandBtnDiv = document.createElement('div');
const remoteExpandBtn = document.createElement('button');
const remoteExpandContainerDiv = document.createElement('div');
// remote peer name element
remotePeerName.setAttribute('id', peer_id + '_name');
remotePeerName.className = 'videoPeerName';
const peerVideoText = document.createTextNode(peer_name);
remotePeerName.appendChild(peerVideoText);
// remote hand status element
remoteHandStatusIcon.setAttribute('id', peer_id + '_handStatus');
remoteHandStatusIcon.style.setProperty('color', 'rgb(0, 255, 0)');
remoteHandStatusIcon.className = className.handPulsate;
// remote video status element
remoteVideoStatusIcon.setAttribute('id', peer_id + '_videoStatus');
remoteVideoStatusIcon.className = className.videoOn;
remoteVideoStatusIcon.style.cursor = 'default';
// remote audio status element
remoteAudioStatusIcon.setAttribute('id', peer_id + '_audioStatus');
remoteAudioStatusIcon.className = className.audioOn;
remoteAudioStatusIcon.style.cursor = 'default';
// remote audio volume element
remoteAudioVolume.setAttribute('id', peer_id + '_audioVolume');
remoteAudioVolume.type = 'range';
remoteAudioVolume.min = 0;
remoteAudioVolume.max = 100;
remoteAudioVolume.value = 100;
// remote private message
remotePrivateMsgBtn.setAttribute('id', peer_id + '_privateMsg');
remotePrivateMsgBtn.className = className.msgPrivate;
// remote geo location
remoteGeoLocationBtn.setAttribute('id', peer_id + '_geoLocation');
remoteGeoLocationBtn.className = className.geoLocation;
// remote share file
remoteFileShareBtn.setAttribute('id', peer_id + '_shareFile');
remoteFileShareBtn.className = className.shareFile;
// remote peer YouTube video
remoteVideoAudioUrlBtn.setAttribute('id', peer_id + '_videoAudioUrl');
remoteVideoAudioUrlBtn.className = className.shareVideoAudio;
// my video to image
remoteVideoToImgBtn.setAttribute('id', peer_id + '_snapshot');
remoteVideoToImgBtn.className = className.snapShot;
// remote peer kick out
remotePeerKickOut.setAttribute('id', peer_id + '_kickOut');
remotePeerKickOut.className = className.kickOut;
// remote video zoomIn/Out
remoteVideoZoomInBtn.setAttribute('id', peer_id + 'videoZoomIn');
remoteVideoZoomInBtn.className = className.zoomIn;
remoteVideoZoomOutBtn.setAttribute('id', peer_id + 'videoZoomOut');
remoteVideoZoomOutBtn.className = className.zoomOut;
// remote video Picture in Picture
remoteVideoPiPBtn.setAttribute('id', peer_id + 'videoPIP');
remoteVideoPiPBtn.className = className.pip;
// remote video full screen mode
remoteVideoFullScreenBtn.setAttribute('id', peer_id + '_fullScreen');
remoteVideoFullScreenBtn.className = className.fullScreen;
// remote video pin/unpin button
remoteVideoPinBtn.setAttribute('id', peer_id + '_pinUnpin');
remoteVideoPinBtn.className = className.pinUnpin;
// remote video hide all button
remoteVideoFocusBtn.setAttribute('id', peer_id + '_focusMode');
remoteVideoFocusBtn.className = className.hideAll;
// remote video toggle mirror
remoteVideoMirrorBtn.setAttribute('id', peer_id + '_toggleMirror');
remoteVideoMirrorBtn.className = className.mirror;
// no mobile devices
if (!isMobileDevice) {
setTippy(remotePeerName, 'Participant name', 'bottom');
setTippy(remoteHandStatusIcon, 'Participant hand is raised', 'bottom');
setTippy(remoteVideoStatusIcon, 'Participant video is on', 'bottom');
setTippy(remoteAudioStatusIcon, 'Participant audio is on', 'bottom');
setTippy(remoteAudioVolume, '🔊 Volume', 'top');
setTippy(remoteVideoAudioUrlBtn, 'Send Video or Audio', 'bottom');
setTippy(remotePrivateMsgBtn, 'Send private message', 'bottom');
setTippy(remoteGeoLocationBtn, 'Get Geo Location', 'bottom');
setTippy(remoteFileShareBtn, 'Send file', 'bottom');
setTippy(remoteVideoToImgBtn, 'Take a snapshot', 'bottom');
setTippy(remotePeerKickOut, 'Kick out', 'bottom');
setTippy(remoteVideoFullScreenBtn, 'Full screen mode', 'bottom');
setTippy(remoteVideoZoomInBtn, 'Zoom in video', 'bottom');
setTippy(remoteVideoZoomOutBtn, 'Zoom out video', 'bottom');
setTippy(remoteVideoPiPBtn, 'Toggle picture in picture', 'bottom');
setTippy(remoteVideoPinBtn, 'Toggle Pin video', 'bottom');
setTippy(remoteVideoFocusBtn, 'Toggle Focus mode', 'bottom');
setTippy(remoteVideoMirrorBtn, 'Toggle video mirror', 'bottom');
}
// my video avatar image
remoteVideoAvatarImage.setAttribute('id', peer_id + '_avatar');
remoteVideoAvatarImage.className = 'videoAvatarImage'; // pulsate
// remote pitch meter
remotePitchMeter.setAttribute('id', peer_id + '_pitch');
remotePitchBar.setAttribute('id', peer_id + '_pitch_bar');
remotePitchMeter.className = 'speechbar';
remotePitchBar.className = 'bar';
remotePitchBar.style.height = '1%';
remotePitchMeter.appendChild(remotePitchBar);
// remote video nav bar
remoteVideoNavBar.className = 'navbar fadein';
// remote expand buttons div
remoteExpandBtnDiv.className = 'expand-video';
remoteExpandBtn.id = peer_id + '_videoExpandBtn';
remoteExpandBtn.className = 'fas fa-ellipsis-vertical';
remoteExpandContainerDiv.className = 'expand-video-content';
// attach to remote video nav bar
!isMobileDevice && remoteVideoNavBar.appendChild(remoteVideoPinBtn);
buttons.remote.showVideoFocusBtn && remoteVideoNavBar.appendChild(remoteVideoFocusBtn);
remoteVideoNavBar.appendChild(remoteVideoMirrorBtn);
if (showVideoPipBtn && buttons.remote.showVideoPipBtn) remoteVideoNavBar.appendChild(remoteVideoPiPBtn);
// Add to expand container div...
if (buttons.remote.showZoomInOutBtn) {
remoteExpandContainerDiv.appendChild(remoteVideoZoomInBtn);
remoteExpandContainerDiv.appendChild(remoteVideoZoomOutBtn);
}
buttons.remote.showPrivateMessageBtn && remoteExpandContainerDiv.appendChild(remotePrivateMsgBtn);
buttons.remote.showGeoLocationBtn && remoteExpandContainerDiv.appendChild(remoteGeoLocationBtn);
buttons.remote.showFileShareBtn && remoteExpandContainerDiv.appendChild(remoteFileShareBtn);
buttons.remote.showShareVideoAudioBtn && remoteExpandContainerDiv.appendChild(remoteVideoAudioUrlBtn);
buttons.remote.showKickOutBtn && remoteExpandContainerDiv.appendChild(remotePeerKickOut);
remoteExpandBtnDiv.appendChild(remoteExpandBtn);
remoteExpandBtnDiv.appendChild(remoteExpandContainerDiv);
isVideoFullScreenSupported && remoteVideoNavBar.appendChild(remoteVideoFullScreenBtn);
buttons.remote.showSnapShotBtn && remoteVideoNavBar.appendChild(remoteVideoToImgBtn);
remoteVideoNavBar.appendChild(remoteVideoStatusIcon);
remoteVideoNavBar.appendChild(remoteAudioStatusIcon);
// Disabled audio volume control on Mobile devices
if (!isMobileDevice && peer_audio && buttons.remote.showAudioVolume) {
remoteVideoNavBar.appendChild(remoteAudioVolume);
}
remoteVideoNavBar.appendChild(remoteHandStatusIcon);
remoteVideoNavBar.appendChild(remoteExpandBtnDiv);
remoteMedia.setAttribute('id', peer_id + '___video');
remoteMedia.setAttribute('playsinline', true);
remoteMedia.autoplay = true;
remoteMediaControls = isMobileDevice ? false : remoteMediaControls;
remoteMedia.style.objectFit = peer_screen_status ? 'contain' : 'var(--video-object-fit)';
remoteMedia.style.name = peer_id + (peer_screen_status ? '_typeScreen' : '_typeCam');
remoteMedia.controls = remoteMediaControls;
remoteMedia.poster = images.poster;
remoteVideoWrap.className = 'Camera';
remoteVideoWrap.setAttribute('id', peer_id + '_videoWrap');
remoteVideoWrap.style.display = isHideALLVideosActive ? 'none' : 'block';
// add elements to videoWrap div
remoteVideoWrap.appendChild(remoteVideoNavBar);
remoteVideoWrap.appendChild(remoteVideoAvatarImage);
remoteVideoWrap.appendChild(remotePitchMeter);
remoteVideoWrap.appendChild(remoteMedia);
remoteVideoWrap.appendChild(remotePeerName);
// need later on disconnect or remove peers
peerVideoMediaElements[remoteMedia.id] = remoteVideoWrap;
// append all elements to videoMediaContainer
videoMediaContainer.appendChild(remoteVideoWrap);
// attachMediaStream is a part of the adapter.js library
attachMediaStream(remoteMedia, stream);
// resize video elements
adaptAspectRatio();
// handle video to image
buttons.remote.showSnapShotBtn && handleVideoToImg(remoteMedia.id, remoteVideoToImgBtn.id, peer_id);
// handle video pin/unpin
handleVideoPinUnpin(remoteMedia.id, remoteVideoPinBtn.id, remoteVideoWrap.id, peer_id, peer_screen_status);
// handle video focus mode
handleVideoFocusMode(remoteVideoFocusBtn, remoteVideoWrap, remoteMedia);
// handle video toggle mirror
handleVideoToggleMirror(remoteMedia.id, remoteVideoMirrorBtn.id);
// handle vide picture in picture
if (showVideoPipBtn && buttons.remote.showVideoPipBtn)
handlePictureInPicture(remoteVideoPiPBtn.id, remoteMedia.id, peer_id);
// handle video zoomIn/Out
ZOOM_IN_OUT_ENABLED &&
handleVideoZoomInOut(remoteVideoZoomInBtn.id, remoteVideoZoomOutBtn.id, remoteMedia.id, peer_id);
// pin video on screen share detected
if (peer_video_status && peer_screen_status) {
remoteVideoPinBtn.click();
}
// handle video full screen mode
isVideoFullScreenSupported && handleVideoPlayerFs(remoteMedia.id, remoteVideoFullScreenBtn.id, peer_id);
// handle file share drag and drop
handleFileDragAndDrop(remoteMedia.id, peer_id);
// handle kick out button event
buttons.remote.showKickOutBtn && handlePeerKickOutBtn(peer_id);
// set video privacy true
peer_privacy_status && setVideoPrivacyStatus(remoteMedia.id, peer_privacy_status);
// refresh remote peers avatar name
setPeerAvatarImgName(remoteVideoAvatarImage.id, peer_name, peer_avatar);
// refresh remote peers hand icon status and title
setPeerHandStatus(peer_id, peer_name, peer_hand_status);
// refresh remote peers video icon status and title
setPeerVideoStatus(peer_id, peer_screen_status ? peer_screen_status : peer_video_status);
// refresh remote peers audio icon status and title
setPeerAudioStatus(peer_id, peer_audio_status);
// handle remote peers audio on-off
handlePeerAudioBtn(peer_id);
// handle remote peers video on-off
handlePeerVideoBtn(peer_id);
// handle remote geo location
buttons.remote.showGeoLocationBtn && handlePeerGeoLocation(peer_id, peer_name);
// handle remote private messages
buttons.remote.showPrivateMessageBtn && handlePeerPrivateMsg(peer_id, peer_name);
// handle remote send file
buttons.remote.showFileShareBtn && handlePeerSendFile(peer_id);
// handle remote video - audio URL
buttons.remote.showShareVideoAudioBtn && handlePeerVideoAudioUrl(peer_id);
// show status menu
toggleClassElements('statusMenu', 'inline');
// notify if peer started to recording own screen + audio
if (peer_rec_status) notifyRecording(peer_id, peer_name, peer_avatar, 'Started');
// Peer without camera, screen sharing OFF
if (!peer_video && !peer_screen_status) {
elemDisplay(remoteVideoAvatarImage, true, 'block');
remoteVideoStatusIcon.className = className.videoOff;
}
// Peer without camera, screen sharing ON
if (!peer_video && peer_screen_status) {
handleScreenStart(peer_id);
}
break;
case 'audio':
// alert('remote audio');
console.log('SETUP REMOTE AUDIO STREAM');
// handle remote audio elements
const remoteAudioWrap = document.createElement('div');
const remoteAudioMedia = document.createElement('audio');
const remoteAudioVolumeId = peer_id + '_audioVolume';
remoteAudioMedia.id = peer_id + '___audio';
remoteAudioMedia.autoplay = true;
remoteAudioMedia.audio = 1.0;
remoteAudioWrap.appendChild(remoteAudioMedia);
audioMediaContainer.appendChild(remoteAudioWrap);
attachMediaStream(remoteAudioMedia, stream);
peerAudioMediaElements[remoteAudioMedia.id] = remoteAudioWrap;
// handle remote peers audio volume
handleAudioVolume(remoteAudioVolumeId, remoteAudioMedia.id);
// Toggle visibility of volume control based on the audio status of the peer
elemDisplay(getId(remoteAudioVolumeId), peer_audio_status);
// Change audio output...
if (sinkId && audioOutputSelect.value) await changeAudioDestination(remoteAudioMedia);
break;
default:
break;
}
}
/**
* Log stream settings info
* @param {string} name function name called from
* @param {object} stream media stream audio - video
*/
function logStreamSettingsInfo(name, stream) {
if ((useVideo || isScreenStreaming) && hasVideoTrack(stream)) {
console.log(name, {
video: {
label: stream.getVideoTracks()[0].label,
settings: stream.getVideoTracks()[0].getSettings(),
},
});
}
if (useAudio && hasAudioTrack(stream)) {
console.log(name, {
audio: {
label: stream.getAudioTracks()[0].label,
settings: stream.getAudioTracks()[0].getSettings(),
},
});
}
}
/**
* Handle aspect ratio
* ['0:0', '4:3', '16:9', '1:1', '1:2'];
* 0 1 2 3 4
*/
function adaptAspectRatio() {
const participantsCount = videoMediaContainer.childElementCount;
if (peersCount) peersCount.innerText = participantsCount;
let desktop,
mobile = 1;
// desktop aspect ratio
switch (participantsCount) {
// case 1:
// desktop = 0; // (0:0)
// break;
case 1:
case 3:
case 4:
case 7:
case 9:
desktop = 2; // (16:9)
break;
case 5:
case 6:
case 10:
case 11:
desktop = 1; // (4:3)
break;
case 2:
case 8:
desktop = 3; // (1:1)
break;
default:
desktop = 0; // (0:0)
}
// mobile aspect ratio
switch (participantsCount) {
case 3:
case 9:
case 10:
mobile = 2; // (16:9)
break;
case 2:
case 7:
case 8:
case 11:
mobile = 1; // (4:3)
break;
case 1:
case 4:
case 5:
case 6:
mobile = 3; // (1:1)
break;
default:
mobile = 3; // (1:1)
}
if (participantsCount > 11) {
desktop = 1; // (4:3)
mobile = 3; // (1:1)
}
setAspectRatio(isMobileDevice ? mobile : desktop);
}
/**
* Get Gravatar from email
* @param {string} email
* @param {integer} size
* @returns object image
*/
function genGravatar(email, size = false) {
const hash = md5(email.toLowerCase().trim());
const gravatarURL = `https://www.gravatar.com/avatar/${hash}` + (size ? `?s=${size}` : '?s=250');
return gravatarURL;
function md5(input) {
return CryptoJS.MD5(input).toString();
}
}
/**
* Check if valid email
* @param {string} email
* @returns boolean
*/
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Create round svg image with first 2 letters of peerName in center
* Thank you: https://github.com/phpony
*
* @param {string} peerName
* @param {integer} avatarImgSize width and height in px
*/
function 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;');
}
/**
* Refresh video - chat image avatar on name changes: https://eu.ui-avatars.com/
* @param {string} videoAvatarImageId element id
* @param {string} peerName
* @param {string} peerAvatar
*/
function setPeerAvatarImgName(videoAvatarImageId, peerName, peerAvatar) {
const videoAvatarImageElement = getId(videoAvatarImageId);
videoAvatarImageElement.style.pointerEvents = 'none';
// If a valid avatar image URL is provided
if (peerAvatar && isImageURL(peerAvatar)) {
videoAvatarImageElement.setAttribute('src', peerAvatar);
}
// If not, use SVG based on the email validity
else if (useAvatarSvg) {
const avatarImgSize = isMobileDevice ? 128 : 256;
const avatarImgSvg = isValidEmail(peerName) ? genGravatar(peerName) : genAvatarSvg(peerName, avatarImgSize);
videoAvatarImageElement.setAttribute('src', avatarImgSvg);
}
// Default fallback avatar
else {
videoAvatarImageElement.setAttribute('src', images.avatar);
}
}
/**
* Set Chat avatar image by peer name
* @param {string} avatar position left/right
* @param {string} peerName me or peer name
* @param {string} peerAvatar me or peer avatar
*/
function setPeerChatAvatarImgName(avatar, peerName, peerAvatar) {
const avatarImg =
peerAvatar && isImageURL(peerAvatar)
? peerAvatar
: isValidEmail(peerName)
? genGravatar(peerName)
: genAvatarSvg(peerName, 32);
switch (avatar) {
case 'left':
// console.log("Set Friend chat avatar image");
leftChatAvatar = avatarImg;
break;
case 'right':
// console.log("Set My chat avatar image");
rightChatAvatar = avatarImg;
break;
default:
break;
}
}
/**
* Handle Video Toggle Mirror
* @param {string} videoId
* @param {string} videoToggleMirrorBtnId
*/
function handleVideoToggleMirror(videoId, videoToggleMirrorBtnId) {
const videoPlayer = getId(videoId);
const videoToggleMirrorBtn = getId(videoToggleMirrorBtnId);
if (videoPlayer && videoToggleMirrorBtn) {
// Toggle video mirror
videoToggleMirrorBtn.addEventListener('click', (e) => {
videoPlayer.classList.toggle('mirror');
});
}
}
/**
* On video player click, go on full screen mode ||
* On button click, go on full screen mode.
* Press Esc to exit from full screen mode, or click again.
* @param {string} videoId uuid video element
* @param {string} videoFullScreenBtnId uuid full screen btn
* @param {string} peer_id socket.id
*/
function handleVideoPlayerFs(videoId, videoFullScreenBtnId, peer_id = null) {
const videoPlayer = getId(videoId);
const videoFullScreenBtn = getId(videoFullScreenBtnId);
// handle Chrome Firefox Opera Microsoft Edge videoPlayer ESC
videoPlayer.addEventListener('fullscreenchange', (e) => {
// if Controls enabled, or document on FS do nothing
if (videoPlayer.controls || isDocumentOnFullScreen) return;
const fullscreenElement = document.fullscreenElement;
if (!fullscreenElement) {
videoPlayer.style.pointerEvents = 'auto';
isVideoOnFullScreen = false;
// console.log("Esc FS isVideoOnFullScreen", isVideoOnFullScreen);
}
});
// handle Safari videoPlayer ESC
videoPlayer.addEventListener('webkitfullscreenchange', (e) => {
// if Controls enabled, or document on FS do nothing
if (videoPlayer.controls || isDocumentOnFullScreen) return;
const webkitIsFullScreen = document.webkitIsFullScreen;
if (!webkitIsFullScreen) {
videoPlayer.style.pointerEvents = 'auto';
isVideoOnFullScreen = false;
// console.log("Esc FS isVideoOnFullScreen", isVideoOnFullScreen);
}
});
// on button click go on FS mobile/desktop
videoFullScreenBtn.addEventListener('click', (e) => {
if (videoPlayer.classList.contains('videoCircle')) {
return userLog('toast', 'Full Screen not allowed if video on privacy mode');
}
gotoFS();
});
// on video click go on FS
videoPlayer.addEventListener('click', (e) => {
if (videoPlayer.classList.contains('videoCircle')) {
return userLog('toast', 'Full Screen not allowed if video on privacy mode');
}
// not mobile on click go on FS or exit from FS
if (!isMobileDevice) {
gotoFS();
} else {
// mobile on click exit from FS, for enter use videoFullScreenBtn
if (isVideoOnFullScreen) handleFSVideo();
}
});
function gotoFS() {
// handle remote peer video fs
if (peer_id !== null) {
const remoteVideoStatusBtn = getId(peer_id + '_videoStatus');
if (remoteVideoStatusBtn.className === className.videoOn) {
handleFSVideo();
} else {
showMsg();
}
} else {
// handle local video fs
if (myVideoStatusIcon.className === className.videoOn || isScreenStreaming) {
handleFSVideo();
} else {
showMsg();
}
}
}
function showMsg() {
userLog('toast', 'Full screen mode work when video is on');
}
function handleFSVideo() {
// if Controls enabled, or document on FS do nothing
if (videoPlayer.controls || isDocumentOnFullScreen) return;
if (!isVideoOnFullScreen) {
if (videoPlayer.requestFullscreen) {
// Chrome Firefox Opera Microsoft Edge
videoPlayer.requestFullscreen();
} else if (videoPlayer.webkitRequestFullscreen) {
// Safari request full screen mode
videoPlayer.webkitRequestFullscreen();
} else if (videoPlayer.msRequestFullscreen) {
// IE11 request full screen mode
videoPlayer.msRequestFullscreen();
}
isVideoOnFullScreen = true;
videoPlayer.style.pointerEvents = 'none';
// console.log("Go on FS isVideoOnFullScreen", isVideoOnFullScreen);
} else {
if (document.exitFullscreen) {
// Chrome Firefox Opera Microsoft Edge
document.exitFullscreen();
} else if (document.webkitCancelFullScreen) {
// Safari exit full screen mode ( Not work... )
document.webkitCancelFullScreen();
} else if (document.msExitFullscreen) {
// IE11 exit full screen mode
document.msExitFullscreen();
}
isVideoOnFullScreen = false;
videoPlayer.style.pointerEvents = 'auto';
// console.log("Esc FS isVideoOnFullScreen", isVideoOnFullScreen);
}
}
}
/**
* Handle file drag and drop on video element
* @param {string} elemId element id
* @param {string} peer_id peer id
* @param {boolean} itsMe true/false
*/
function handleFileDragAndDrop(elemId, peer_id, itsMe = false) {
const videoPeer = getId(elemId);
videoPeer.addEventListener('dragover', function (e) {
e.preventDefault();
e.stopPropagation();
e.target.parentElement.style.outline = '3px dashed var(--dd-color)';
document.querySelector('.Camera').style.outline = 'none';
});
videoPeer.addEventListener('dragleave', function (e) {
e.preventDefault();
e.stopPropagation();
e.target.parentElement.style.outline = 'none';
});
videoPeer.addEventListener('drop', function (e) {
e.preventDefault();
e.stopPropagation();
e.target.parentElement.style.outline = 'none';
if (itsMe) {
return userLog('warning', 'You cannot send files to yourself.');
}
if (sendInProgress) {
return userLog('warning', 'Please wait for the previous file to be sent.');
}
if (e.dataTransfer.items && e.dataTransfer.items.length > 1) {
return userLog('warning', 'Please drag and drop a single file.');
}
// Use DataTransferItemList interface to access the file(s)
if (e.dataTransfer.items) {
// If dropped items aren't files, reject them
const 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');
}
const file = e.dataTransfer.items[0].getAsFile();
sendFileInformations(file, peer_id);
} else {
// Use DataTransfer interface to access the file(s)
sendFileInformations(e.dataTransfer.files[0], peer_id);
}
});
}
/**
* Handle video privacy button click event
* @param {string} videoId
* @param {boolean} privacyBtnId
*/
function handleVideoPrivacyBtn(videoId, privacyBtnId) {
const video = getId(videoId);
const privacyBtn = getId(privacyBtnId);
if (useVideo && video && privacyBtn) {
privacyBtn.addEventListener('click', () => {
playSound('click');
isVideoPrivacyActive = !isVideoPrivacyActive;
setVideoPrivacyStatus(videoId, isVideoPrivacyActive);
emitPeerStatus('privacy', isVideoPrivacyActive);
});
} else {
if (privacyBtn) elemDisplay(privacyBtn, false);
}
}
/**
* Set video privacy status
* @param {string} peerVideoId
* @param {boolean} peerPrivacyActive
*/
function setVideoPrivacyStatus(peerVideoId, peerPrivacyActive) {
const video = getId(peerVideoId);
if (!video) return;
if (peerPrivacyActive) {
video.classList.remove('videoDefault');
video.classList.add('videoCircle');
video.style.objectFit = 'cover';
} else {
video.classList.remove('videoCircle');
video.classList.add('videoDefault');
video.style.objectFit = 'var(--video-object-fit)';
}
}
/**
* Handle video pin/unpin
* @param {string} elemId video id
* @param {string} pnId button pin id
* @param {string} camId video wrap id
* @param {string} peerId peer id
* @param {boolean} isScreen stream
*/
function handleVideoPinUnpin(elemId, pnId, camId, peerId, isScreen = false) {
const videoPlayer = getId(elemId);
const btnPn = getId(pnId);
const cam = getId(camId);
if (btnPn && videoPlayer && cam) {
btnPn.addEventListener('click', () => {
if (isMobileDevice) return;
playSound('click');
isVideoPinned = !isVideoPinned;
if (isVideoPinned) {
if (!videoPlayer.classList.contains('videoCircle')) {
videoPlayer.style.objectFit = 'contain';
}
cam.className = '';
cam.style.width = '100%';
cam.style.height = '100%';
toggleVideoPin(pinVideoPositionSelect.value);
videoPinMediaContainer.appendChild(cam);
elemDisplay(videoPinMediaContainer, true, 'block');
pinnedVideoPlayerId = elemId;
setColor(btnPn, 'lime');
} else {
if (pinnedVideoPlayerId != videoPlayer.id) {
isVideoPinned = true;
if (isScreenEnabled) return;
return userLog('toast', 'Another video seems pinned, unpin it before to pin this one', 5000);
}
if (!isScreenStreaming) videoPlayer.style.objectFit = 'var(--video-object-fit)';
if (isScreen || videoPlayer.style.name == peerId + '_typeScreen')
videoPlayer.style.objectFit = 'contain';
videoPinMediaContainer.removeChild(cam);
cam.className = 'Camera';
videoMediaContainer.appendChild(cam);
removeVideoPinMediaContainer(peerId, true);
setColor(btnPn, 'white');
}
adaptAspectRatio();
});
}
}
function toggleVideoPin(position) {
if (!isVideoPinned) return;
switch (position) {
case 'top':
videoPinMediaContainer.style.top = '25%';
videoPinMediaContainer.style.width = '100%';
videoPinMediaContainer.style.height = '70%';
videoMediaContainer.style.top = 0;
videoMediaContainer.style.width = '100%';
videoMediaContainer.style.height = '25%';
videoMediaContainer.style.right = 0;
break;
case 'vertical':
videoPinMediaContainer.style.top = 0;
videoPinMediaContainer.style.width = '75%';
videoPinMediaContainer.style.height = '100%';
videoMediaContainer.style.top = 0;
videoMediaContainer.style.width = '25%';
videoMediaContainer.style.height = '100%';
videoMediaContainer.style.right = 0;
break;
case 'horizontal':
videoPinMediaContainer.style.top = 0;
videoPinMediaContainer.style.width = '100%';
videoPinMediaContainer.style.height = '75%';
videoMediaContainer.style.top = '75%';
videoMediaContainer.style.right = null;
videoMediaContainer.style.width = null;
videoMediaContainer.style.width = '100% !important';
videoMediaContainer.style.height = '25%';
break;
default:
break;
}
resizeVideoMedia();
}
/**
* Handle video focus mode (hide all except selected one)
* @param {object} remoteVideoFocusBtn button
* @param {object} remoteVideoWrap videoWrapper
* @param {object} remoteMedia videoMedia
*/
function handleVideoFocusMode(remoteVideoFocusBtn, remoteVideoWrap, remoteMedia) {
if (remoteVideoFocusBtn) {
remoteVideoFocusBtn.addEventListener('click', (e) => {
if (isHideMeActive) {
return userLog('toast', 'To use this feature, please toggle Hide self view before', 'top-end', 6000);
}
isHideALLVideosActive = !isHideALLVideosActive;
e.target.style.color = isHideALLVideosActive ? 'lime' : 'white';
if (isHideALLVideosActive) {
remoteVideoWrap.style.width = '100%';
remoteVideoWrap.style.height = '100%';
remoteMedia.setAttribute('focus-mode', 'true');
} else {
resizeVideoMedia();
remoteMedia.removeAttribute('focus-mode');
}
const children = videoMediaContainer.children;
for (let child of children) {
if (child.id != remoteVideoWrap.id) {
child.style.display = isHideALLVideosActive ? 'none' : 'block';
}
}
});
}
}
/**
* Zoom in/out video element center or by cursor position
* @param {string} zoomInBtnId
* @param {string} zoomOutBtnId
* @param {string} mediaId
* @param {string} peerId
*/
function handleVideoZoomInOut(zoomInBtnId, zoomOutBtnId, mediaId, peerId = null) {
const id = peerId ? peerId + '_videoStatus' : 'myVideoStatusIcon';
const videoWrap = getId(peerId ? peerId + '_videoWrap' : 'myVideoWrap');
const zoomIn = getId(zoomInBtnId);
const zoomOut = getId(zoomOutBtnId);
const video = getId(mediaId);
/**
* 1.1: This value is used when the `zoomDirection` is 'zoom-in'.
* It means that when the user scrolls the mouse wheel up (indicating a zoom-in action), the scale factor is set to 1.1.
* This means that the content will be scaled up to 110% of its original size with each scroll event, effectively making it larger.
*/
const ZOOM_IN_FACTOR = 1.1;
/**
* 0.9: This value is used when the zoomDirection is 'zoom-out'.
* It means that when the user scrolls the mouse wheel down (indicating a zoom-out action), the scale factor is set to 0.9.
* This means that the content will be scaled down to 90% of its original size with each scroll event, effectively making it smaller.
*/
const ZOOM_OUT_FACTOR = 0.9;
const MAX_ZOOM = 15;
const MIN_ZOOM = 1;
let zoom = 1;
function setTransform() {
if (isVideoOf(id) || isVideoPrivacyMode(video)) return;
zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
video.style.scale = zoom;
}
function resetZoom(video) {
zoom = 1;
video.style.transform = '';
video.style.transformOrigin = 'center';
}
if (!isMobileDevice) {
// Zoom center
if (ZOOM_CENTER_MODE) {
video.addEventListener('wheel', (e) => {
e.preventDefault();
let delta = e.wheelDelta ? e.wheelDelta : -e.deltaY;
delta > 0 ? (zoom *= 1.2) : (zoom /= 1.2);
setTransform();
});
} else {
// Zoom on cursor position
video.addEventListener('wheel', (e) => {
e.preventDefault();
if (isVideoOf(id) || isVideoPrivacyMode(video)) 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));
video.style.transformOrigin = `${cursorX}px ${cursorY}px`;
video.style.transform = `scale(${zoom})`;
video.style.cursor = zoom === 1 ? 'pointer' : zoomDirection;
});
videoWrap.addEventListener('mouseleave', () => {
video.style.cursor = 'pointer';
if (video.id === myVideo.id && !isScreenStreaming) {
resetZoom(video);
}
});
video.addEventListener('mouseleave', () => {
video.style.cursor = 'pointer';
});
}
}
if (buttons.local.showZoomInOutBtn) {
zoomIn.addEventListener('click', () => {
if (isVideoOf(id)) return userLog('toast', 'Zoom in work when video is on');
if (isVideoPrivacyMode(video)) return userLog('toast', 'Zoom in not allowed if video on privacy mode');
zoom = zoom + 0.1;
setTransform();
});
zoomOut.addEventListener('click', () => {
if (isVideoOf(id)) return userLog('toast', 'Zoom out work when video is on');
if (isVideoPrivacyMode(video)) return userLog('toast', 'Zoom out not allowed if video on privacy mode');
zoom = zoom - 0.1;
setTransform();
});
}
function isVideoOf(id) {
const videoStatusBtn = getId(id);
return videoStatusBtn.className === className.videoOff;
}
function isVideoPrivacyMode() {
return video.classList.contains('videoCircle');
}
}
/**
* Handle Video Picture in Picture mode
*
* @param {string} btnId
* @param {string} videoId
* @param {string} peerId
*/
function handlePictureInPicture(btnId, videoId, peerId) {
const btnPiP = getId(btnId);
const video = getId(videoId);
const myVideoStatus = getId('myVideoStatusIcon');
const remoteVideoStatus = getId(peerId + '_videoStatus');
btnPiP.addEventListener('click', () => {
if (video.pictureInPictureElement) {
video.exitPictureInPicture();
} else if (document.pictureInPictureEnabled) {
if (
(myVideoStatus && myVideoStatus.className === className.videoOff) ||
(remoteVideoStatus && remoteVideoStatus.className === className.videoOff)
) {
return msgPopup('warning', 'Prohibit Picture-in-Picture (PIP) on disabled video', 'top-end', 6000);
}
video.requestPictureInPicture().catch((error) => {
console.error('Failed to enter Picture-in-Picture mode:', error);
msgPopup('warning', error.message, 'top-end', 6000);
elemDisplay(btnPiP, false);
});
}
});
if (video) {
video.addEventListener('leavepictureinpicture', (event) => {
console.log('Exited PiP mode');
// Check if the video is paused
if (video.paused) {
// Play the video again
video.play().catch((error) => {
console.error('Error playing video after exit PIP mode:', error);
});
}
});
}
}
/**
* Remove video pin media container
* @param {string} peer_id aka socket.id
* @param {boolean} force_remove force to remove
*/
function removeVideoPinMediaContainer(peer_id, force_remove = false) {
//alert(pinnedVideoPlayerId + '==' + peer_id);
if (
(isVideoPinned && (pinnedVideoPlayerId == peer_id + '___video' || pinnedVideoPlayerId == peer_id)) ||
force_remove
) {
elemDisplay(videoPinMediaContainer, false);
isVideoPinned = false;
pinnedVideoPlayerId = null;
videoMediaContainerUnpin();
if (isChatPinned) {
chatPin();
}
if (isCaptionPinned) {
captionPin();
}
resizeVideoMedia();
}
}
/**
* Pin videoMediaContainer
*/
function videoMediaContainerPin() {
if (!isVideoPinned) {
videoMediaContainer.style.top = 0;
videoMediaContainer.style.width = '75%';
videoMediaContainer.style.height = '100%';
}
}
/**
* Unpin videoMediaContainer
*/
function videoMediaContainerUnpin() {
if (!isVideoPinned) {
videoMediaContainer.style.top = 0;
videoMediaContainer.style.right = null;
videoMediaContainer.style.width = '100%';
videoMediaContainer.style.height = '100%';
}
}
/**
* Handle Video to Img click event
* @param {string} videoStream uuid video element
* @param {string} videoToImgBtn uuid snapshot btn
* @param {string} peer_id socket.id
*/
function handleVideoToImg(videoStream, videoToImgBtn, peer_id = null) {
const videoTIBtn = getId(videoToImgBtn);
const video = getId(videoStream);
videoTIBtn.addEventListener('click', () => {
if (video.classList.contains('videoCircle')) {
return userLog('toast', 'Snapshot not allowed if video on privacy mode');
}
if (peer_id !== null) {
// handle remote video snapshot
const remoteVideoStatusBtn = getId(peer_id + '_videoStatus');
if (remoteVideoStatusBtn.className === className.videoOn) {
return takeSnapshot(video);
}
} else {
// handle local video snapshot
if (myVideoStatusIcon.className === className.videoOn) {
return takeSnapshot(video);
}
}
userLog('toast', 'Snapshot not work on video disabled');
});
}
/**
* Save Video Frame to Image
* @param {object} video element from where to take the snapshot
*/
function takeSnapshot(video) {
playSound('snapshot');
let context, canvas, width, height, dataURL;
width = video.videoWidth;
height = video.videoHeight;
canvas = canvas || document.createElement('canvas');
canvas.width = width;
canvas.height = height;
context = canvas.getContext('2d');
context.drawImage(video, 0, 0, width, height);
dataURL = canvas.toDataURL('image/png'); // or image/jpeg
// console.log(dataURL);
saveDataToFile(dataURL, getDataTimeString() + '-SNAPSHOT.png');
}
/**
* Start session time
*/
function startSessionTime() {
callElapsedTime = 0;
elemDisplay(mySessionTime, true);
setInterval(function printTime() {
callElapsedTime++;
mySessionTime.innerText = secondsToHms(callElapsedTime);
}, 1000);
}
/**
* Refresh my localVideoMediaStream video status
* @param {MediaStream} localVideoMediaStream
*/
function refreshMyVideoStatus(localVideoMediaStream) {
if (!localVideoMediaStream) return;
// check Track video status
localVideoMediaStream.getTracks().forEach((track) => {
if (track.kind === 'video') {
myVideoStatus = track.enabled;
}
});
}
/**
* Refresh my localAudioMediaStream audio status
* @param {MediaStream} localAudioMediaStream
*/
function refreshMyAudioStatus(localAudioMediaStream) {
if (!localAudioMediaStream) return;
// check Track audio status
localAudioMediaStream.getTracks().forEach((track) => {
if (track.kind === 'audio') {
myAudioStatus = track.enabled;
}
});
}
/**
* Handle WebRTC left buttons
*/
function manageButtons() {
// Buttons bar
setShareRoomBtn();
setRecordStreamBtn();
setScreenShareBtn();
setFullScreenBtn();
setChatRoomBtn();
setCaptionRoomBtn();
setRoomEmojiButton();
setChatEmojiBtn();
setMyWhiteboardBtn();
setSnapshotRoomBtn();
setMyFileShareBtn();
setDocumentPiPBtn();
setMySettingsBtn();
setAboutBtn();
// Buttons bottom
setToggleExtraButtons();
setAudioBtn();
setVideoBtn();
setSwapCameraBtn();
setHideMeButton();
setMyHandBtn();
setLeaveRoomBtn();
}
/**
* Copy - share room url button click event
*/
function setShareRoomBtn() {
shareRoomBtn.addEventListener('click', async (e) => {
shareRoomUrl();
});
shareRoomBtn.addEventListener('mouseenter', () => {
if (isMobileDevice || !buttons.main.showShareQr) return;
elemDisplay(qrRoomPopupContainer, true);
});
shareRoomBtn.addEventListener('mouseleave', () => {
if (isMobileDevice || !buttons.main.showShareQr) return;
elemDisplay(qrRoomPopupContainer, false);
});
}
/**
* Hide myself from room view
*/
function setHideMeButton() {
hideMeBtn.addEventListener('click', (e) => {
if (isHideALLVideosActive) {
return userLog('toast', 'To use this feature, please toggle video focus mode', 'top-end', 6000);
}
isHideMeActive = !isHideMeActive;
handleHideMe(isHideMeActive);
});
}
/**
* Toggle extra buttons
*/
function setToggleExtraButtons() {
toggleExtraBtn.addEventListener('click', () => {
toggleExtraButtons();
if (!isMobileDevice) {
isToggleExtraBtnClicked = true;
setTimeout(() => {
isToggleExtraBtnClicked = false;
}, 2000);
}
});
toggleExtraBtn.addEventListener('mouseover', () => {
if (isToggleExtraBtnClicked || isMobileDevice) return;
if (buttonsBar.style.display === 'none') {
toggleExtraButtons();
}
});
}
/**
* Toggle extra buttons
*/
function toggleExtraButtons() {
const isButtonsBarHidden = buttonsBar.style.display === 'none' || buttonsBar.style.display === '';
const displayValue = isButtonsBarHidden ? 'flex' : 'none';
const cName = isButtonsBarHidden ? className.up : className.down;
elemDisplay(buttonsBar, isButtonsBarHidden, displayValue);
toggleExtraBtn.className = cName;
}
/**
* Audio mute - unmute button click event
*/
function setAudioBtn() {
audioBtn.addEventListener('click', (e) => {
handleAudio(e, false);
});
document.onkeydown = (e) => {
if (!isPushToTalkActive || isChatRoomVisible) return;
if (e.code === 'Space') {
if (isSpaceDown) return; // prevent multiple call
handleAudio(audioBtn, false, true);
isSpaceDown = true;
console.log('Push-to-talk: audio ON');
}
};
document.onkeyup = (e) => {
e.preventDefault();
if (!isPushToTalkActive || isChatRoomVisible) return;
if (e.code === 'Space') {
handleAudio(audioBtn, false, false);
isSpaceDown = false;
console.log('Push-to-talk: audio OFF');
}
};
}
/**
* Video hide - show button click event
*/
function setVideoBtn() {
videoBtn.addEventListener('click', async (e) => {
await handleVideo(e, false);
});
}
/**
* Check if can swap or not the cam, if yes show the button else hide it
*/
function setSwapCameraBtn() {
navigator.mediaDevices.enumerateDevices().then((devices) => {
const videoInput = devices.filter((device) => device.kind === 'videoinput');
if (videoInput.length > 1 && isMobileDevice) {
swapCameraBtn.addEventListener('click', (e) => {
swapCamera();
});
} else {
elemDisplay(swapCameraBtn, false);
}
});
}
/**
* Check if i can share the screen, if yes show button else hide it
*/
function setScreenShareBtn() {
if (
!isMobileDevice &&
(navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia) &&
buttons.main.showScreenBtn
) {
isScreenSharingSupported = true;
initScreenShareBtn.addEventListener('click', async (e) => {
await toggleScreenSharing(true);
});
screenShareBtn.addEventListener('click', async (e) => {
await toggleScreenSharing();
});
} else {
elemDisplay(initScreenShareBtn, false);
elemDisplay(screenShareBtn, false);
elemDisplay(screenFpsDiv, false);
}
}
/**
* Start - Stop Stream recording
*/
function setRecordStreamBtn() {
recordStreamBtn.addEventListener('click', (e) => {
if (isStreamRecording) {
stopStreamRecording();
} else {
startStreamRecording();
}
});
recImage.addEventListener('click', (e) => {
recordStreamBtn.click();
});
}
/**
* Full screen button click event
*/
function setFullScreenBtn() {
const fsSupported =
buttons.main.showFullScreenBtn &&
(document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled);
if (fsSupported) {
// detect esc from full screen mode
document.addEventListener('fullscreenchange', (e) => {
let fullscreenElement = document.fullscreenElement;
if (!fullscreenElement) {
fullScreenBtn.className = className.fsOff;
isDocumentOnFullScreen = false;
setTippy(fullScreenBtn, 'View full screen', placement);
}
});
fullScreenBtn.addEventListener('click', (e) => {
toggleFullScreen();
});
} else {
elemDisplay(fullScreenBtn, false);
}
}
/**
* Chat room buttons click event
*/
function setChatRoomBtn() {
// adapt chat room size for mobile
setChatRoomAndCaptionForMobile();
// Search peer by name
searchPeerBarName.addEventListener('keyup', () => {
searchPeer();
});
// open hide chat room
chatRoomBtn.addEventListener('click', (e) => {
if (!isChatRoomVisible) {
showChatRoomDraggable();
} else {
hideChatRoomAndEmojiPicker();
e.target.className = className.chatOn;
}
});
// pin/unpin
msgerTogglePin.addEventListener('click', () => {
toggleChatPin();
});
// ghost theme + undo
msgerTheme.addEventListener('click', (e) => {
if (e.target.className == className.ghost) {
e.target.className = className.undo;
setSP('--msger-bg', 'rgba(0, 0, 0, 0.100)');
} else {
e.target.className = className.ghost;
setTheme();
}
});
// dropdown chat menu
msgerDropDownMenuBtn.addEventListener('click', () => {
toggleChatDropDownMenu();
});
// show msger participants section
msgerCPBtn.addEventListener('click', (e) => {
if (!thereArePeerConnections()) {
return userLog('info', 'No participants detected');
}
elemDisplay(msgerCP, true, 'flex');
});
// hide msger participants section
msgerCPCloseBtn.addEventListener('click', (e) => {
elemDisplay(msgerCP, false);
});
// clean chat messages
msgerClean.addEventListener('click', (e) => {
if (chatMessages.length != 0) {
return cleanMessages();
}
userLog('info', 'No chat messages to delete');
});
// save chat messages to file
msgerSaveBtn.addEventListener('click', (e) => {
if (chatMessages.length != 0) {
return downloadChatMsgs();
}
userLog('info', 'No chat messages to save');
});
// close chat room - show left button and status menu if hide
msgerClose.addEventListener('click', (e) => {
chatMinimize();
hideChatRoomAndEmojiPicker();
showButtonsBarAndMenu();
});
// Maximize chat
msgerMaxBtn.addEventListener('click', (e) => {
chatMaximize();
});
// minimize chat
msgerMinBtn.addEventListener('click', (e) => {
chatMinimize();
});
// Markdown on-off
msgerMarkdownBtn.addEventListener('click', (e) => {
isChatMarkdownOn = !isChatMarkdownOn;
setColor(msgerMarkdownBtn, isChatMarkdownOn ? 'lime' : 'white');
});
// ChatGPT/OpenAI
msgerGPTBtn.addEventListener('click', (e) => {
isChatGPTOn = !isChatGPTOn;
setColor(msgerGPTBtn, isChatGPTOn ? 'lime' : 'white');
});
// share file from chat
msgerShareFileBtn.addEventListener('click', (e) => {
e.preventDefault();
selectFileToShare(myPeerId, true);
});
// open Video Url Player
msgerVideoUrlBtn.addEventListener('click', (e) => {
sendVideoUrl();
});
// Execute a function when the user releases a key on the keyboard
msgerInput.addEventListener('keyup', (e) => {
// Number 13 is the "Enter" key on the keyboard
if (e.keyCode === 13 && (isMobileDevice || !e.shiftKey)) {
e.preventDefault();
msgerSendBtn.click();
}
});
// on input check 4emoji from map
msgerInput.oninput = function () {
if (isChatPasteTxt) return;
for (let i in chatInputEmoji) {
let regex = new RegExp(escapeSpecialChars(i), 'gim');
this.value = this.value.replace(regex, chatInputEmoji[i]);
}
checkLineBreaks();
};
msgerInput.onpaste = () => {
isChatPasteTxt = true;
checkLineBreaks();
};
// clean input msg txt
msgerCleanTextBtn.addEventListener('click', (e) => {
cleanMessageInput();
});
// paste to input msg txt
msgerPasteBtn.addEventListener('click', (e) => {
pasteToMessageInput();
});
// chat show on message
msgerShowChatOnMsg.addEventListener('change', (e) => {
playSound('switch');
showChatOnMessage = e.currentTarget.checked;
showChatOnMessage
? msgPopup('info', 'Chat will be shown, when you receive a new message', 'top-end', 3000)
: msgPopup('info', 'Chat not will be shown, when you receive a new message', 'top-end', 3000);
lsSettings.show_chat_on_msg = showChatOnMessage;
lS.setSettings(lsSettings);
});
// speech incoming message
if (isSpeechSynthesisSupported) {
msgerSpeechMsg.addEventListener('change', (e) => {
playSound('switch');
speechInMessages = e.currentTarget.checked;
speechInMessages
? msgPopup('info', 'When You receive a new message, it will be converted into speech', 'top-end', 3000)
: msgPopup('info', 'You have disabled speech messages', 'top-end', 3000);
lsSettings.speech_in_msg = speechInMessages;
lS.setSettings(lsSettings);
});
} else {
elemDisplay(msgerSpeechMsgDiv, false);
}
// chat send msg
msgerSendBtn.addEventListener('click', async (e) => {
// prevent refresh page
e.preventDefault();
await sendChatMessage();
});
// adapt input font size 4 mobile
if (isMobileDevice) msgerInput.style.fontSize = 'xx-small';
}
/**
* Caption room buttons click event
*/
function setCaptionRoomBtn() {
if (buttons.main.showCaptionRoomBtn) {
// open hide caption
captionBtn.addEventListener('click', (e) => {
if (!isCaptionBoxVisible) {
showCaptionDraggable();
} else {
hideCaptionBox();
}
});
// Maximize caption
captionMaxBtn.addEventListener('click', (e) => {
captionMaximize();
});
// minimize caption
captionMinBtn.addEventListener('click', (e) => {
captionMinimize();
});
// toggle caption pin
captionTogglePin.addEventListener('click', () => {
toggleCaptionPin();
});
// ghost theme + undo
captionTheme.addEventListener('click', (e) => {
if (e.target.className == className.ghost) {
e.target.className = className.undo;
setSP('--msger-bg', 'rgba(0, 0, 0, 0.100)');
} else {
e.target.className = className.ghost;
setTheme();
}
});
// clean caption transcripts
captionClean.addEventListener('click', (e) => {
if (transcripts.length != 0) {
return cleanCaptions();
}
userLog('info', 'No captions to delete');
});
// save caption transcripts to file
captionSaveBtn.addEventListener('click', (e) => {
if (transcripts.length != 0) {
return downloadCaptions();
}
userLog('info', 'No captions to save');
});
// close caption box - show left button and status menu if hide
captionClose.addEventListener('click', (e) => {
captionMinimize();
hideCaptionBox();
showButtonsBarAndMenu();
});
// hide it
elemDisplay(speechRecognitionStop, false);
if (speechRecognition) {
// start recognition speech
speechRecognitionStart.addEventListener('click', (e) => {
startSpeech();
});
// stop recognition speech
speechRecognitionStop.addEventListener('click', (e) => {
stopSpeech();
});
} else {
elemDisplay(captionFooter, false);
}
} else {
elemDisplay(captionBtn, false);
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API#browser_compatibility
}
}
/**
* Set room emoji reaction button
*/
function setRoomEmojiButton() {
const pickerRoomOptions = {
theme: 'dark',
onEmojiSelect: sendEmojiToRoom,
};
const emojiRoomPicker = new EmojiMart.Picker(pickerRoomOptions);
emojiPickerContainer.appendChild(emojiRoomPicker);
elemDisplay(emojiPickerContainer, false);
if (!isMobileDevice) {
dragElement(emojiPickerContainer, emojiPickerHeader);
}
roomEmojiPickerBtn.addEventListener('click', (e) => {
toggleEmojiPicker();
});
closeEmojiPickerContainer.addEventListener('click', (e) => {
toggleEmojiPicker();
});
function sendEmojiToRoom(data) {
console.log('Selected Emoji', data);
const message = {
type: 'roomEmoji',
room_id: roomId,
peer_name: myPeerName,
emoji: data.native,
shortcodes: data.shortcodes,
};
if (thereArePeerConnections()) {
sendToServer('message', message);
}
handleEmoji(message);
}
function toggleEmojiPicker() {
if (emojiPickerContainer.style.display === 'block') {
elemDisplay(emojiPickerContainer, false);
setColor(roomEmojiPickerBtn, 'var(--btn-bar-bg-color)');
} else {
emojiPickerContainer.style.display = 'block';
setColor(roomEmojiPickerBtn, 'yellow');
}
}
}
/**
* Emoji picker chat room button click event
*/
function setChatEmojiBtn() {
msgerEmojiBtn.addEventListener('click', (e) => {
// prevent refresh page
e.preventDefault();
hideShowEmojiPicker();
});
// Add emoji picker
const pickerOptions = {
theme: 'dark',
onEmojiSelect: addEmojiToMsg,
};
const emojiPicker = new EmojiMart.Picker(pickerOptions);
msgerEmojiPicker.appendChild(emojiPicker);
}
/**
* Add emoji to chat message
*/
function addEmojiToMsg(data) {
//console.log(data);
msgerInput.value += data.native;
hideShowEmojiPicker();
}
/**
* Set my hand button click event
*/
function setMyHandBtn() {
myHandBtn.addEventListener('click', async (e) => {
setMyHandStatus();
});
}
/**
* Whiteboard: https://github.com/fabricjs/fabric.js
*/
function setMyWhiteboardBtn() {
dragElement(whiteboard, whiteboardHeader);
setupWhiteboard();
whiteboardBtn.addEventListener('click', (e) => {
handleWhiteboardToggle();
});
whiteboardPencilBtn.addEventListener('click', (e) => {
whiteboardIsDrawingMode(true);
});
whiteboardObjectBtn.addEventListener('click', (e) => {
whiteboardIsDrawingMode(false);
});
whiteboardUndoBtn.addEventListener('click', (e) => {
whiteboardAction(getWhiteboardAction('undo'));
});
whiteboardRedoBtn.addEventListener('click', (e) => {
whiteboardAction(getWhiteboardAction('redo'));
});
whiteboardDropDownMenuBtn.addEventListener('click', function () {
whiteboardDropdownMenu.style.display === 'block'
? elemDisplay(whiteboardDropdownMenu, false)
: elemDisplay(whiteboardDropdownMenu, true, 'block');
});
whiteboardSaveBtn.addEventListener('click', (e) => {
wbCanvasSaveImg();
});
whiteboardImgFileBtn.addEventListener('click', (e) => {
whiteboardAddObj('imgFile');
});
whiteboardPdfFileBtn.addEventListener('click', (e) => {
whiteboardAddObj('pdfFile');
});
whiteboardImgUrlBtn.addEventListener('click', (e) => {
whiteboardAddObj('imgUrl');
});
whiteboardTextBtn.addEventListener('click', (e) => {
whiteboardAddObj('text');
});
whiteboardLineBtn.addEventListener('click', (e) => {
whiteboardAddObj('line');
});
whiteboardRectBtn.addEventListener('click', (e) => {
whiteboardAddObj('rect');
});
whiteboardTriangleBtn.addEventListener('click', (e) => {
whiteboardAddObj('triangle');
});
whiteboardCircleBtn.addEventListener('click', (e) => {
whiteboardAddObj('circle');
});
whiteboardEraserBtn.addEventListener('click', (e) => {
whiteboardIsEraser(true);
});
whiteboardCleanBtn.addEventListener('click', (e) => {
confirmCleanBoard();
});
whiteboardLockBtn.addEventListener('click', (e) => {
toggleLockUnlockWhiteboard();
});
whiteboardUnlockBtn.addEventListener('click', (e) => {
toggleLockUnlockWhiteboard();
});
whiteboardCloseBtn.addEventListener('click', (e) => {
handleWhiteboardToggle();
});
wbDrawingColorEl.addEventListener('change', (e) => {
wbCanvas.freeDrawingBrush.color = wbDrawingColorEl.value;
whiteboardIsDrawingMode(true);
});
wbBackgroundColorEl.addEventListener('change', (e) => {
setWhiteboardBgColor(wbBackgroundColorEl.value);
});
whiteboardGhostButton.addEventListener('click', (e) => {
wbIsBgTransparent = !wbIsBgTransparent;
//setWhiteboardBgColor(wbIsBgTransparent ? 'rgba(0, 0, 0, 0.100)' : wbBackgroundColorEl.value);
wbIsBgTransparent ? wbCanvasBackgroundColor('rgba(0, 0, 0, 0.100)') : setTheme();
});
// Hide the whiteboard dropdown menu if clicked outside
document.addEventListener('click', (event) => {
if (!whiteboardDropDownMenuBtn.contains(event.target) && !whiteboardDropDownMenuBtn.contains(event.target)) {
elemDisplay(whiteboardDropdownMenu, false);
}
});
}
/**
* File Transfer button click event
*/
function setMyFileShareBtn() {
// make send-receive file div draggable
if (!isMobileDevice) {
dragElement(sendFileDiv, imgShareSend);
dragElement(receiveFileDiv, imgShareReceive);
}
fileShareBtn.addEventListener('click', (e) => {
//window.open("https://fromsmash.com"); // for Big Data
selectFileToShare(myPeerId, true);
});
sendAbortBtn.addEventListener('click', (e) => {
abortFileTransfer();
});
receiveAbortBtn.addEventListener('click', (e) => {
abortReceiveFileTransfer();
});
receiveHideBtn.addEventListener('click', (e) => {
hideFileTransfer();
});
}
/**
* Set snapshot room button click event
*/
function setSnapshotRoomBtn() {
snapshotRoomBtn.addEventListener('click', async (e) => {
await snapshotRoom();
});
}
/**
* Snapshot Screen, Window or Tab
*/
async function snapshotRoom() {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const video = document.createElement('video');
try {
const captureStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
});
video.srcObject = captureStream;
video.onloadedmetadata = () => {
video.play();
};
// Wait for the video to start playing
video.onplay = async () => {
playSound('snapshot');
// Sleep some ms
await sleep(1000);
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// Create a link element to download the image
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = 'Room_' + roomId + '_' + getDataTimeString() + '_snapshot.png';
link.click();
// Stop all video tracks to release the capture stream
captureStream.getTracks().forEach((track) => track.stop());
// Clean up: remove references to avoid memory leaks
video.srcObject = null;
canvas.width = 0;
canvas.height = 0;
};
} catch (err) {
console.error('Error: ' + err);
userLog('error', 'Snapshot room error ' + err.message, 6000);
}
}
/**
* Document Picture-in-Picture button click event
*/
function setDocumentPiPBtn() {
documentPiPBtn.addEventListener('click', async () => {
if (!showDocumentPipBtn) return;
if (documentPictureInPicture.window) {
documentPictureInPicture.window.close();
console.log('DOCUMENT PIP close');
return;
}
await documentPictureInPictureOpen();
});
}
/**
* Restart documentPictureInPicture
* @returns void
*/
async function documentPictureInPictureRestart() {
if (!showDocumentPipBtn || !documentPictureInPicture.window) return;
documentPictureInPictureClose();
setTimeout(async () => {
await documentPictureInPictureOpen();
}, 300);
}
/**
* Close documentPictureInPicture
*/
async function documentPictureInPictureClose() {
if (!showDocumentPipBtn) return;
if (documentPictureInPicture.window) {
documentPictureInPicture.window.close();
console.log('DOCUMENT PIP close');
}
}
/**
* Open documentPictureInPicture
*/
async function documentPictureInPictureOpen() {
if (!showDocumentPipBtn) return;
try {
const pipWindow = await documentPictureInPicture.requestWindow({
width: 300,
height: 720,
});
function updateCustomProperties() {
const documentStyle = getComputedStyle(document.documentElement);
pipWindow.document.documentElement.style = `
--body-bg: ${documentStyle.getPropertyValue('--body-bg')};
`;
}
updateCustomProperties();
const pipStylesheet = document.createElement('link');
const pipVideoContainer = document.createElement('div');
pipStylesheet.type = 'text/css';
pipStylesheet.rel = 'stylesheet';
pipStylesheet.href = '../css/documentPiP.css';
pipVideoContainer.className = 'pipVideoContainer';
pipWindow.document.head.append(pipStylesheet);
pipWindow.document.body.append(pipVideoContainer);
function cloneVideoElements() {
let foundVideo = false;
pipVideoContainer.innerHTML = '';
[...getSlALL('video')].forEach((video) => {
console.log('DOCUMENT PIP found video id -----> ' + video.id);
// No video stream detected or is video share from URL...
if (!video.srcObject || video.id === 'videoAudioUrlElement') return;
// get video element
const videoPlayer = getId(video.id);
const isLocalVideo = video.id === 'myVideo';
const isPIPAllowed = !videoPlayer.classList.contains('videoCircle'); // not in privacy mode
// Check if video can be add on pipVideo
isLocalVideo
? console.log('DOCUMENT PIP LOCAL: PiP allowed? -----> ' + isPIPAllowed)
: console.log('DOCUMENT PIP REMOTE: PiP allowed? -----> ' + isPIPAllowed);
if (!isPIPAllowed) return;
// Video is ON and not in privacy mode continue....
foundVideo = true;
const pipVideo = document.createElement('video');
pipVideo.classList.add('pipVideo');
pipVideo.classList.toggle('mirror', video.classList.contains('mirror'));
pipVideo.srcObject = video.srcObject;
pipVideo.autoplay = true;
pipVideo.muted = true;
pipVideoContainer.append(pipVideo);
function observeElementClassChanges(element, observerName) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
console.log(
`${observerName}: Element ${mutation.target.id} class changed:`,
mutation.target.className
);
cloneVideoElements(); // Or other desired function
}
});
});
observer.observe(element, { attributes: true, attributeFilter: ['class'] });
return observer;
}
// Start observing for new videos and class changes (Video Privacy ON/OFF)
if (video) observeElementClassChanges(video, 'Video');
// Get videoStatus...
const parts = video.id.split('___');
const peer_id = parts[0];
const videoStatus = getId(isLocalVideo ? 'myVideoStatusIcon' : peer_id + '_videoStatus');
// Start observing for new videosStatus and class changes (video ON/OFF)
if (videoStatus) observeElementClassChanges(videoStatus, 'VideoStatus');
});
return foundVideo;
}
if (!cloneVideoElements()) {
documentPictureInPictureClose();
return userLog('toast', 'No video allowed for Document PIP');
}
const videoObserver = new MutationObserver(() => {
cloneVideoElements();
});
videoObserver.observe(videoMediaContainer, {
childList: true,
});
const documentObserver = new MutationObserver(() => {
updateCustomProperties();
});
documentObserver.observe(document.documentElement, {
attributeFilter: ['style'],
});
pipWindow.addEventListener('unload', () => {
videoObserver.disconnect();
documentObserver.disconnect();
});
} catch (err) {
userLog('warning', err.message, 6000);
}
}
/**
* My settings button click event
*/
function setMySettingsBtn() {
mySettingsBtn.addEventListener('click', (e) => {
if (isMobileDevice) {
elemDisplay(buttonsBar, false);
elemDisplay(bottomButtons, false);
isButtonsVisible = false;
}
hideShowMySettings();
});
mySettingsCloseBtn.addEventListener('click', (e) => {
hideShowMySettings();
});
speakerTestBtn.addEventListener('click', (e) => {
playSound('ring', true);
});
myPeerNameSetBtn.addEventListener('click', (e) => {
updateMyPeerName();
});
// Sounds
switchSounds.addEventListener('change', (e) => {
notifyBySound = e.currentTarget.checked;
lsSettings.sounds = notifyBySound;
lS.setSettings(lsSettings);
userLog('toast', `${icons.sounds} Notify & sounds ` + (notifyBySound ? 'ON' : 'OFF'));
playSound('switch');
});
switchShare.addEventListener('change', (e) => {
notify = e.currentTarget.checked;
lsSettings.share_on_join = notify;
lS.setSettings(lsSettings);
userLog('toast', `${icons.share} Share room on join ` + (notify ? 'ON' : 'OFF'));
playSound('switch');
});
switchKeepButtonsVisible.addEventListener('change', (e) => {
isButtonsBarOver = isKeepButtonsVisible = e.currentTarget.checked;
lsSettings.keep_buttons_visible = isButtonsBarOver;
lS.setSettings(lsSettings);
const status = isButtonsBarOver ? 'enabled' : 'disabled';
userLog('toast', `Buttons always visible ${status}`);
playSound('switch');
});
if (isMobileDevice) {
elemDisplay(pushToTalkDiv, false);
} else {
// Push to talk
switchPushToTalk.addEventListener('change', (e) => {
isPushToTalkActive = e.currentTarget.checked;
userLog('toast', `👆 Push to talk ` + (isPushToTalkActive ? 'ON' : 'OFF'));
playSound('switch');
});
}
switchAudioPitchBar.addEventListener('change', (e) => {
isAudioPitchBar = e.currentTarget.checked;
lsSettings.pitch_bar = isAudioPitchBar;
lS.setSettings(lsSettings);
userLog('toast', `${icons.pitchBar} Audio pitch bar ` + (isAudioPitchBar ? 'ON' : 'OFF'));
playSound('switch');
});
// make chat room draggable for desktop
if (!isMobileDevice) dragElement(mySettings, mySettingsHeader);
// recording codecs
switchH264Recording.addEventListener('change', (e) => {
recPrioritizeH264 = e.currentTarget.checked;
lsSettings.rec_prioritize_h264 = recPrioritizeH264;
lS.setSettings(lsSettings);
userLog('toast', `${icons.codecs} Recording prioritize h.264 ` + (recPrioritizeH264 ? 'ON' : 'OFF'));
playSound('switch');
});
// Recording pause/resume
pauseRecBtn.addEventListener('click', (e) => {
pauseRecording();
});
resumeRecBtn.addEventListener('click', (e) => {
resumeRecording();
});
// Styles
themeCustom.check.onchange = (e) => {
themeCustom.keep = e.currentTarget.checked;
themeSelect.disabled = themeCustom.keep;
lsSettings.theme_custom = themeCustom.keep;
lsSettings.theme_color = themeCustom.color;
lS.setSettings(lsSettings);
setTheme();
userLog('toast', `${icons.theme} Custom theme keep ` + (themeCustom.keep ? 'ON' : 'OFF'));
playSound('switch');
e.target.blur();
};
}
/**
* About button click event
*/
function setAboutBtn() {
aboutBtn.addEventListener('click', (e) => {
showAbout();
});
}
/**
* Leave room button click event
*/
function setLeaveRoomBtn() {
leaveRoomBtn.addEventListener('click', (e) => {
leaveRoom();
});
}
/**
* Handle left buttons - status menù show - hide on body mouse move
*/
function handleBodyOnMouseMove() {
document.body.addEventListener('mousemove', (e) => {
showButtonsBarAndMenu();
});
// detect buttons bar over
buttonsBar.addEventListener('mouseover', () => {
isButtonsBarOver = true;
});
buttonsBar.addEventListener('mouseout', () => {
isButtonsBarOver = false;
});
bottomButtons.addEventListener('mouseover', () => {
isButtonsBarOver = true;
});
bottomButtons.addEventListener('mouseout', () => {
isButtonsBarOver = false;
});
checkButtonsBarAndMenu();
}
/**
* Setup local audio - video devices - theme ...
*/
function setupMySettings() {
// tab buttons
tabRoomBtn.addEventListener('click', (e) => {
openTab(e, 'tabRoom');
});
tabVideoBtn.addEventListener('click', (e) => {
openTab(e, 'tabVideo');
});
tabAudioBtn.addEventListener('click', (e) => {
openTab(e, 'tabAudio');
});
tabVideoShareBtn.addEventListener('click', (e) => {
openTab(e, 'tabMedia');
});
tabRecordingBtn.addEventListener('click', (e) => {
openTab(e, 'tabRecording');
});
tabParticipantsBtn.addEventListener('click', (e) => {
openTab(e, 'tabParticipants');
});
tabProfileBtn.addEventListener('click', (e) => {
openTab(e, 'tabProfile');
});
tabShortcutsBtn.addEventListener('click', (e) => {
openTab(e, 'tabShortcuts');
});
tabNetworkBtn.addEventListener('click', (e) => {
openTab(e, 'tabNetwork');
});
tabStylingBtn.addEventListener('click', (e) => {
openTab(e, 'tabStyling');
});
tabLanguagesBtn.addEventListener('click', (e) => {
openTab(e, 'tabLanguages');
});
// copy room URL
myRoomId.addEventListener('click', () => {
isMobileDevice ? shareRoomUrl() : copyRoomURL();
});
// send invite by email to join room in a specified data-time
roomSendEmailBtn.addEventListener('click', () => {
shareRoomByEmail();
});
// tab media
shareMediaAudioVideoBtn.addEventListener('click', (e) => {
sendVideoUrl();
});
// select audio input
audioInputSelect.addEventListener('change', async () => {
await changeLocalMicrophone(audioInputSelect.value);
refreshLsDevices();
});
// audio options
switchNoiseSuppression.onchange = async (e) => {
const noiseSuppressionEnabled = e.currentTarget.checked;
lsSettings.mic_noise_suppression = noiseSuppressionEnabled;
lS.setSettings(lsSettings);
noiseSuppressionEnabled ? await enableNoiseSuppression() : await disableNoiseSuppression();
toastMessage(
noiseSuppressionEnabled ? 'success' : 'info',
`Noise suppression ${noiseSuppressionEnabled ? 'enabled' : 'disabled'}`
);
e.target.blur();
};
// select audio output
audioOutputSelect.addEventListener('change', async () => {
await changeAudioDestination();
refreshLsDevices();
});
// select video input
videoSelect.addEventListener('change', async () => {
await changeLocalCamera(videoSelect.value);
await handleLocalCameraMirror();
await documentPictureInPictureRestart();
refreshLsDevices();
});
// select video quality
videoQualitySelect.addEventListener('change', async (e) => {
await setLocalVideoQuality();
});
// Firefox may not handle well...
if (isFirefox) {
elemDisplay(videoFpsDiv, false);
}
// select video fps
videoFpsSelect.addEventListener('change', (e) => {
videoMaxFrameRate = parseInt(videoFpsSelect.value, 10);
setLocalMaxFps(videoMaxFrameRate);
lsSettings.video_fps = e.currentTarget.selectedIndex;
lS.setSettings(lsSettings);
});
// select screen fps
screenFpsSelect.addEventListener('change', (e) => {
screenMaxFrameRate = parseInt(screenFpsSelect.value, 10);
if (isScreenStreaming) setLocalMaxFps(screenMaxFrameRate, 'screen');
lsSettings.screen_fps = e.currentTarget.selectedIndex;
lS.setSettings(lsSettings);
});
// Mobile not support screen sharing
if (isMobileDevice) {
screenFpsSelect.value = null;
screenFpsSelect.disabled = true;
}
// select themes
themeSelect.addEventListener('change', (e) => {
lsSettings.theme = themeSelect.selectedIndex;
lS.setSettings(lsSettings);
setTheme();
});
// video object fit
videoObjFitSelect.addEventListener('change', (e) => {
lsSettings.video_obj_fit = videoObjFitSelect.selectedIndex;
lS.setSettings(lsSettings);
setSP('--video-object-fit', videoObjFitSelect.value);
});
// Mobile not support buttons bar position horizontal
if (isMobileDevice) {
btnsBarSelect.disabled = true;
} else {
btnsBarSelect.addEventListener('change', (e) => {
lsSettings.buttons_bar = btnsBarSelect.selectedIndex;
lS.setSettings(lsSettings);
setButtonsBarPosition(btnsBarSelect.value);
resizeMainButtons();
});
}
// Mobile not support pin/unpin video
if (!isMobileDevice) {
pinVideoPositionSelect.addEventListener('change', (e) => {
lsSettings.pin_grid = pinVideoPositionSelect.selectedIndex;
lS.setSettings(lsSettings);
toggleVideoPin(pinVideoPositionSelect.value);
});
} else {
elemDisplay(pinUnpinGridDiv, false);
}
// room actions
captionEveryoneBtn.addEventListener('click', (e) => {
sendToServer('caption', {
room_id: roomId,
peer_name: myPeerName,
action: 'start',
data: {
recognitionLanguageIndex: recognitionLanguage.selectedIndex,
recognitionDialectIndex: recognitionDialect.selectedIndex,
},
});
});
muteEveryoneBtn.addEventListener('click', (e) => {
disableAllPeers('audio');
});
hideEveryoneBtn.addEventListener('click', (e) => {
disableAllPeers('video');
});
ejectEveryoneBtn.addEventListener('click', (e) => {
ejectEveryone();
});
lockRoomBtn.addEventListener('click', (e) => {
handleRoomAction({ action: 'lock' }, true);
});
unlockRoomBtn.addEventListener('click', (e) => {
handleRoomAction({ action: 'unlock' }, true);
});
}
/**
* Handle keyboard shortcuts
*/
function handleShortcuts() {
if (!isDesktopDevice || !buttons.settings.showShortcutsBtn) {
elemDisplay(tabShortcutsBtn, false);
setKeyboardShortcuts(false);
} else {
switchShortcuts.addEventListener('change', (e) => {
const status = setKeyboardShortcuts(e.currentTarget.checked);
userLog('toast', `Keyboard shortcuts ${status}`);
playSound('switch');
});
document.addEventListener('keydown', (event) => {
if (!isShortcutsEnabled || isChatRoomVisible || wbIsOpen) return;
const notPresenter = isRulesActive && !isPresenter;
const key = event.key.toLowerCase(); // Convert to lowercase for simplicity
console.log(`Detected shortcut: ${key}`);
switch (key) {
case 'a':
if (notPresenter && !buttons.main.showAudioBtn) {
toastMessage('warning', 'The presenter has disabled your ability to enable audio');
break;
}
audioBtn.click();
break;
case 'v':
if (notPresenter && !buttons.main.showVideoBtn) {
toastMessage('warning', 'The presenter has disabled your ability to enable video');
break;
}
videoBtn.click();
break;
case 's':
if (notPresenter && !buttons.main.showScreenBtn) {
toastMessage('warning', 'The presenter has disabled your ability to share the screen');
break;
}
screenShareBtn.click();
break;
case 'h':
if (notPresenter && !buttons.main.showMyHandBtn) {
toastMessage('warning', 'The presenter has disabled your ability to raise your hand');
break;
}
myHandBtn.click();
break;
case 'c':
if (notPresenter && !buttons.main.showChatRoomBtn) {
toastMessage('warning', 'The presenter has disabled your ability to open the chat');
break;
}
chatRoomBtn.click();
break;
case 'o':
if (notPresenter && !buttons.main.showMySettingsBtn) {
toastMessage('warning', 'The presenter has disabled your ability to open the settings');
break;
}
mySettingsBtn.click();
break;
case 'x':
if (notPresenter && !button.main.showHideMeBtn) {
toastMessage('warning', 'The presenter has disabled your ability to hide yourself');
break;
}
hideMeBtn.click();
break;
case 'r':
if (notPresenter && !buttons.main.showRecordStreamBtn) {
toastMessage('warning', 'The presenter has disabled your ability to start recording');
break;
}
recordStreamBtn.click();
break;
case 'e':
if (notPresenter && !buttons.main.showRoomEmojiPickerBtn) {
toastMessage('warning', 'The presenter has disabled your ability to open the room emoji');
break;
}
roomEmojiPickerBtn.click();
break;
case 'k':
if (notPresenter && !buttons.main.showCaptionRoomBtn) {
toastMessage('warning', 'The presenter has disabled your ability to start transcription');
break;
}
captionBtn.click();
break;
case 'w':
if (notPresenter && !buttons.main.showWhiteboardBtn) {
toastMessage('warning', 'The presenter has disabled your ability to open the whiteboard');
break;
}
whiteboardBtn.click();
break;
case 'd':
if (!showDocumentPipBtn) {
toastMessage('warning', 'The document PIP is not supported in this browser');
break;
}
if (notPresenter && !buttons.main.showDocumentPipBtn) {
toastMessage('warning', 'The presenter has disabled your ability to open the document PIP');
break;
}
documentPiPBtn.click();
break;
case 't':
if (notPresenter && !buttons.main.showSnapshotRoomBtn) {
toastMessage('warning', 'The presenter has disabled your ability to take a snapshot');
break;
}
snapshotRoomBtn.click();
break;
case 'f':
if (notPresenter && !buttons.settings.showFileShareBtn) {
toastMessage('warning', 'The presenter has disabled your ability to share files');
break;
}
fileShareBtn.click();
break;
//...
default:
console.log(`Unhandled shortcut key: ${key}`);
}
});
}
}
/**
* Set Keyboard Shortcuts enabled
* @param {boolean} enabled
* @return {String} enabled/disabled
*/
function setKeyboardShortcuts(enabled) {
isShortcutsEnabled = enabled;
lsSettings.keyboard_shortcuts = isShortcutsEnabled;
lS.setSettings(lsSettings);
return isShortcutsEnabled ? 'enabled' : 'disabled';
}
/**
* Load settings from local storage
*/
function loadSettingsFromLocalStorage() {
showChatOnMessage = lsSettings.show_chat_on_msg;
speechInMessages = lsSettings.speech_in_msg;
msgerShowChatOnMsg.checked = showChatOnMessage;
msgerSpeechMsg.checked = speechInMessages;
screenFpsSelect.selectedIndex = lsSettings.screen_fps;
videoFpsSelect.selectedIndex = lsSettings.video_fps;
screenFpsSelectedIndex = screenFpsSelect.selectedIndex;
videoFpsSelectedIndex = videoFpsSelect.selectedIndex;
screenMaxFrameRate = parseInt(getSelectedIndexValue(screenFpsSelect), 10);
videoMaxFrameRate = parseInt(getSelectedIndexValue(videoFpsSelect), 10);
notifyBySound = lsSettings.sounds;
isKeepButtonsVisible = lsSettings.keep_buttons_visible;
isAudioPitchBar = lsSettings.pitch_bar;
recPrioritizeH264 = lsSettings.rec_prioritize_h264;
isShortcutsEnabled = lsSettings.keyboard_shortcuts;
switchSounds.checked = notifyBySound;
switchShare.checked = notify;
switchKeepButtonsVisible.checked = isKeepButtonsVisible;
switchAudioPitchBar.checked = isAudioPitchBar;
switchH264Recording.checked = recPrioritizeH264;
switchShortcuts.checked = isShortcutsEnabled;
themeCustom.check.checked = themeCustom.keep;
themeSelect.disabled = themeCustom.keep;
themeCustom.input.value = themeCustom.color;
switchNoiseSuppression.checked = lsSettings.mic_noise_suppression;
videoObjFitSelect.selectedIndex = lsSettings.video_obj_fit;
btnsBarSelect.selectedIndex = lsSettings.buttons_bar;
pinVideoPositionSelect.selectedIndex = lsSettings.pin_grid;
setSP('--video-object-fit', videoObjFitSelect.value);
setButtonsBarPosition(btnsBarSelect.value);
toggleVideoPin(pinVideoPositionSelect.value);
resizeMainButtons();
}
/**
* Get value from element selected index
* @param {object} elem
* @returns any value
*/
function getSelectedIndexValue(elem) {
return elem.options[elem.selectedIndex].value;
}
/**
* Make video Url player draggable
*/
function setupVideoUrlPlayer() {
if (isMobileDevice) {
// adapt video player iframe for mobile
setSP('--iframe-width', '320px');
setSP('--iframe-height', '240px');
} else {
dragElement(videoUrlCont, videoUrlHeader);
dragElement(videoAudioUrlCont, videoAudioUrlHeader);
}
videoUrlCloseBtn.addEventListener('click', (e) => {
e.preventDefault();
closeVideoUrlPlayer();
emitVideoPlayer('close');
});
videoAudioCloseBtn.addEventListener('click', (e) => {
e.preventDefault();
closeVideoUrlPlayer();
emitVideoPlayer('close');
});
}
/**
* Handle Camera mirror logic
*/
async function handleLocalCameraMirror() {
if (camera === 'environment') {
// Back camera → No mirror
initVideo.classList.remove('mirror');
myVideo.classList.remove('mirror');
} else {
// Disable mirror for rear camera
initVideo.classList.add('mirror');
myVideo.classList.add('mirror');
}
}
/**
* Toggle username emoji
*/
function toggleUsernameEmoji() {
usernameEmoji.classList.toggle('hidden');
}
/**
* Handle username emoji picker
*/
function handleUsernameEmojiPicker() {
const pickerOptions = {
theme: 'dark',
onEmojiSelect: addEmojiToUsername,
};
const emojiUsernamePicker = new EmojiMart.Picker(pickerOptions);
usernameEmoji.appendChild(emojiUsernamePicker);
function addEmojiToUsername(data) {
getId('usernameInput').value += data.native;
toggleUsernameEmoji();
}
}
/**
* Toggle vide mirror
*/
function toggleInitVideoMirror() {
initVideo.classList.toggle('mirror');
myVideo.classList.toggle('mirror');
}
/**
* Get audio - video constraints
* @returns {object} audio - video constraints
*/
async function getAudioVideoConstraints() {
const audioSource = audioInputSelect.value;
const videoSource = videoSelect.value;
let videoConstraints = useVideo;
if (videoConstraints) {
videoConstraints = await getVideoConstraints(videoQualitySelect.value ? videoQualitySelect.value : 'default');
videoConstraints['deviceId'] = videoSource ? { exact: videoSource } : undefined;
}
let audioConstraints = { audio: false };
if (useAudio) {
audioConstraints = getAudioConstraints(audioSource);
}
return {
audioConstraints,
video: videoConstraints,
};
}
/**
* Get video constraints: https://webrtc.github.io/samples/src/content/getusermedia/resolution/
* WebCam resolution: https://webcamtests.com/resolution
* @param {string} videoQuality desired video quality
* @returns {object} video constraints
*/
async function getVideoConstraints(videoQuality) {
const frameRate = videoMaxFrameRate;
// Function to construct constraints with ideal or exact width/height
function createConstraints(width, height, frameRate, isIdeal = false) {
const constraints = {
width: isIdeal ? { ideal: width } : { exact: width },
height: isIdeal ? { ideal: height } : { exact: height },
};
// Only add frameRate for non-Firefox browsers
if (!isFirefox) {
constraints.frameRate = isIdeal ? { ideal: frameRate } : frameRate;
}
return constraints;
}
let constraints = {};
switch (videoQuality) {
case 'default':
forceCamMaxResolutionAndFps
? (constraints = createConstraints(7680, 4320, 60, true)) // 8K resolution, 60fps (ideal)
: (constraints = createConstraints(1280, 720, 30, true)); // HD resolution, 30fps (ideal)
break;
case 'qvgaVideo':
constraints = createConstraints(320, 240, frameRate, isFirefox); // Low bandwidth (exact)
break;
case 'vgaVideo':
constraints = createConstraints(640, 480, frameRate, isFirefox); // Medium bandwidth (exact)
break;
case 'hdVideo':
constraints = createConstraints(1280, 720, frameRate, isFirefox); // High bandwidth (exact)
break;
case 'fhdVideo':
constraints = createConstraints(1920, 1080, frameRate, isFirefox); // Very high bandwidth (exact)
break;
case '2kVideo':
constraints = createConstraints(2560, 1440, frameRate, isFirefox); // Ultra high bandwidth (exact)
break;
case '4kVideo':
constraints = createConstraints(3840, 2160, frameRate, isFirefox); // Ultra high bandwidth (exact)
break;
case '6kVideo':
constraints = createConstraints(6144, 3456, frameRate, isFirefox); // Very ultra high bandwidth (exact)
break;
case '8kVideo':
constraints = createConstraints(7680, 4320, frameRate, isFirefox); // Very ultra high bandwidth (exact)
break;
default:
break;
}
console.log('Get Video constraints', constraints);
return constraints;
}
/**
* Get audio constraints
* @param {string} deviceId audio input device ID
* @returns {object} audio constraints
*/
function getAudioConstraints(deviceId = null) {
let audioConstraints = {};
deviceId ? (audioConstraints.deviceId = deviceId) : (audioConstraints = true);
return {
audio: audioConstraints,
};
}
/**
* Set local max fps: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/applyConstraints
* @param {string} maxFrameRate desired max frame rate
* @param {string} type camera/screen default camera
*/
async function setLocalMaxFps(maxFrameRate, type = 'camera') {
if (!useVideo || !localVideoMediaStream || isFirefox) return;
localVideoMediaStream
.getVideoTracks()[0]
.applyConstraints({ frameRate: maxFrameRate })
.then(() => {
logStreamSettingsInfo('setLocalMaxFps', localVideoMediaStream);
type === 'camera'
? (videoFpsSelectedIndex = videoFpsSelect.selectedIndex)
: (screenFpsSelectedIndex = screenFpsSelect.selectedIndex);
})
.catch((err) => {
console.error('setLocalMaxFps', err);
type === 'camera'
? (videoFpsSelect.selectedIndex = videoFpsSelectedIndex)
: (screenFpsSelect.selectedIndex = screenFpsSelectedIndex);
userLog('error', "Your device doesn't support the selected fps, please select the another one.");
});
}
/**
* Set local video quality: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/applyConstraints
*/
async function setLocalVideoQuality() {
if (!useVideo || !localVideoMediaStream) return;
const videoConstraints = await getVideoConstraints(videoQualitySelect.value ? videoQualitySelect.value : 'default');
localVideoMediaStream
.getVideoTracks()[0]
.applyConstraints(videoConstraints)
.then(() => {
logStreamSettingsInfo('setLocalVideoQuality', localVideoMediaStream);
videoQualitySelectedIndex = videoQualitySelect.selectedIndex;
})
.catch((err) => {
videoQualitySelect.selectedIndex = videoQualitySelectedIndex;
console.error('setLocalVideoQuality', err);
userLog('error', "Your device doesn't support the selected video quality, please select the another one.");
});
}
/**
* Change audio output (Speaker)
*/
async function changeAudioDestination(audioElement = false) {
const audioDestination = audioOutputSelect.value;
if (audioElement) {
// change audio output to specified participant audio
await attachSinkId(audioElement, audioDestination);
} else {
const audioElements = audioMediaContainer.querySelectorAll('audio');
// change audio output for all participants audio
audioElements.forEach(async (audioElement) => {
// discard my own audio on this device, so I won't hear myself.
if (audioElement.id != 'myAudio') {
await attachSinkId(audioElement, audioDestination);
}
});
}
}
/**
* Attach audio output device to audio element using device/sink ID.
* @param {object} element audio element to attach the audio output
* @param {string} sinkId uuid audio output device
*/
async function attachSinkId(element, sinkId) {
if (typeof element.sinkId !== 'undefined') {
element
.setSinkId(sinkId)
.then(() => {
console.log(`Success, audio output device attached: ${sinkId}`);
})
.catch((err) => {
let errorMessage = err;
if (err.name === 'SecurityError') {
errorMessage = 'SecurityError: You need to use HTTPS for selecting audio output device';
} else if (err.name === 'NotAllowedError') {
errorMessage = 'NotAllowedError: Permission to use audio output device is not granted';
} else if (err.name === 'NotFoundError') {
errorMessage = 'NotFoundError: The specified audio output device was not found';
} else {
errorMessage = `Error: ${err}`;
}
console.error(errorMessage);
userLog('error', `attachSinkId: ${errorMessage}`);
// Jump back to first output device in the list as it's the default.
audioOutputSelect.selectedIndex = 0;
});
} else {
console.warn('Browser does not support output device selection.');
}
}
/**
* AttachMediaStream stream to element
* @param {object} element element to attach the stream
* @param {object} stream media stream audio - video
*/
function attachMediaStream(element, stream) {
if (!element || !stream) return;
//console.log("DEPRECATED, attachMediaStream will soon be removed.");
element.srcObject = stream;
console.log('Success, media stream attached', stream.getTracks());
}
/**
* Show left buttons & status
* if buttons visible or I'm on hover do nothing return
* if mobile and chatroom open do nothing return
* if mobile and myCaption visible do nothing
* if mobile and mySettings open do nothing return
*/
function showButtonsBarAndMenu() {
if (
isButtonsBarOver ||
isButtonsVisible ||
(isMobileDevice && isChatRoomVisible) ||
(isMobileDevice && isCaptionBoxVisible) ||
(isMobileDevice && isMySettingsVisible)
)
return;
toggleClassElements('navbar', 'block');
//elemDisplay(buttonsBar, true, 'flex');
toggleExtraBtn.className = className.down;
elemDisplay(bottomButtons, true, 'flex');
isButtonsVisible = true;
}
/**
* Check every 10 sec if need to hide buttons bar and status menu
*/
function checkButtonsBarAndMenu() {
if (lsSettings.keep_buttons_visible) {
toggleClassElements('navbar', 'block');
toggleExtraBtn.className = className.up;
elemDisplay(buttonsBar, true, 'flex');
elemDisplay(bottomButtons, true, 'flex');
isButtonsVisible = true;
} else {
if (!isButtonsBarOver) {
toggleClassElements('navbar', 'none');
toggleExtraBtn.className = className.up;
elemDisplay(buttonsBar, false);
elemDisplay(bottomButtons, false);
isButtonsVisible = false;
}
}
setTimeout(() => {
checkButtonsBarAndMenu();
}, 10000);
}
/**
* Copy room url to clipboard and share it with navigator share if supported
* https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
*/
async function shareRoomUrl() {
// navigator share
if (navigator.share) {
try {
// not add title and description to load metadata from url
const roomURL = getRoomURL();
await navigator.share({ url: roomURL });
userLog('toast', 'Room Shared successfully!');
} catch (err) {
/*
This feature is available only in secure contexts (HTTPS),
in some or all supporting browsers and mobile devices
console.error("navigator.share", err);
*/
console.error('Navigator share error', err);
shareRoomMeetingURL();
}
} else {
shareRoomMeetingURL();
}
}
/**
* Share meeting room
* @param {boolean} checkScreen check screen share
*/
function shareRoomMeetingURL(checkScreen = false) {
playSound('newMessage');
const roomURL = getRoomURL();
Swal.fire({
background: swBg,
position: 'center',
title: 'Share the room',
html: `
<div id="qrRoomContainer">
<canvas id="qrRoom"></canvas>
</div>
<br/>
<p style="color:rgb(8, 189, 89);">Join from your mobile device</p>
<p style="background:transparent; color:white; font-family: Arial, Helvetica, sans-serif;">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</p>
<p style="color:rgb(8, 189, 89);">${roomURL}</p>`,
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 room
if (checkScreen) checkShareScreen();
});
makeRoomQR();
}
/**
* Make Room QR
* https://github.com/neocotic/qrious
*/
function makeRoomQR() {
const qr = new QRious({
element: getId('qrRoom'),
value: window.location.href,
});
qr.set({
size: 256,
});
}
/**
* Make Room Popup QR
*/
function makeRoomPopupQR() {
const qr = new QRious({
element: document.getElementById('qrRoomPopup'),
value: window.location.href,
});
qr.set({
size: 256,
});
}
/**
* Copy Room URL to clipboard
*/
function copyRoomURL() {
const roomURL = getRoomURL();
const 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);
console.log('Copied to clipboard Join Link ', roomURL);
document.body.removeChild(tmpInput);
userLog('toast', 'Meeting URL copied to clipboard 👍');
}
/**
* Send the room ID via email at the scheduled date and time.
*/
function shareRoomByEmail() {
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
imageUrl: images.message,
position: 'center',
title: 'Select a Date and Time',
html: '<input type="text" id="datetimePicker" class="flatpickr" />',
showCancelButton: true,
confirmButtonText: 'OK',
cancelButtonColor: 'red',
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
preConfirm: () => {
const roomURL = getRoomURL();
const newLine = '%0D%0A%0D%0A';
const selectedDateTime = document.getElementById('datetimePicker').value;
const roomPassword = isRoomLocked && thisRoomPassword ? 'Password: ' + thisRoomPassword + newLine : '';
const email = '';
const emailSubject = `Please join our MiroTalk P2P 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,
});
}
/**
* Get Room URL
* @returns {url} roomURL
*/
function getRoomURL() {
return myRoomUrl;
// return isHostProtected && isPeerAuthEnabled
// ? window.location.origin + '/join/?room=' + roomId + '&token=' + myToken
// : myRoomUrl;
}
/**
* Handle Audio ON - OFF
* @param {object} e event
* @param {boolean} init on join room
* @param {null|boolean} force audio off (default null can be true/false)
*/
function handleAudio(e, init, force = null) {
if (!useAudio) return;
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/getAudioTracks
const audioStatus = force !== null ? force : !myAudioStatus;
const audioClassName = audioStatus ? className.audioOn : className.audioOff;
myAudioStatus = audioStatus;
localAudioMediaStream.getAudioTracks()[0].enabled = audioStatus;
force != null ? (e.className = audioClassName) : (e.target.className = audioClassName);
audioBtn.className = audioClassName;
if (init) {
initAudioBtn.className = audioClassName;
setTippy(initAudioBtn, audioStatus ? 'Stop the audio' : 'Start the audio', 'right');
initMicrophoneSelect.disabled = !audioStatus;
initSpeakerSelect.disabled = !audioStatus;
lS.setInitConfig(lS.MEDIA_TYPE.audio, audioStatus);
}
setMyAudioStatus(myAudioStatus);
}
/**
* Stop audio track from MediaStream
* @param {MediaStream} stream
*/
async function stopAudioTracks(stream) {
if (!stream) return;
stream.getTracks().forEach((track) => {
if (track.kind === 'audio') track.stop();
});
}
/**
* Handle Video ON - OFF
* @param {object} e event
* @param {boolean} init on join room
* @param {null|boolean} force video off (default null can be true/false)
*/
async function handleVideo(e, init, force = null) {
if (!useVideo) return;
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/getVideoTracks
const videoStatus = force !== null ? force : !myVideoStatus;
const videoClassName = videoStatus ? className.videoOn : className.videoOff;
myVideoStatus = videoStatus;
localVideoMediaStream.getVideoTracks()[0].enabled = videoStatus;
force != null ? (e.className = videoClassName) : (e.target.className = videoClassName);
videoBtn.className = videoClassName;
if (init) {
initVideoBtn.className = videoClassName;
setTippy(initVideoBtn, videoStatus ? 'Stop the video' : 'Start the video', 'top');
videoStatus ? elemDisplay(initVideo, true, 'block') : elemDisplay(initVideo, false);
initVideoSelect.disabled = !videoStatus;
lS.setInitConfig(lS.MEDIA_TYPE.video, videoStatus);
initVideoContainerShow(videoStatus);
elemDisplay(initVideoMirrorBtn, videoStatus);
}
if (!videoStatus) {
if (!isScreenStreaming) {
// Stop the video track based on the condition
init
? await stopVideoTracks(initStream) // Stop init video track (camera LED off)
: await stopVideoTracks(localVideoMediaStream); // Stop local video track (camera LED off)
}
} else {
init
? await changeInitCamera(initVideoSelect.value) // Resume the video track for the init camera (camera LED on)
: await changeLocalCamera(videoSelect.value); // Resume the video track for the local camera (camera LED on)
}
setMyVideoStatus(videoStatus);
}
/**
* Handle initVideoContainer
* @param {boolean} show
*/
function initVideoContainerShow(show = true) {
initVideoContainer.style.width = show ? '100%' : 'auto';
}
/**
* Stop video track from MediaStream
* @param {MediaStream} stream
*/
async function stopVideoTracks(stream) {
if (!stream) return;
stream.getTracks().forEach((track) => {
if (track.kind === 'video') track.stop();
});
}
/**
* SwapCamera front (user) - rear (environment)
*/
async function swapCamera() {
// setup camera
let camVideo = false;
camera = camera == 'user' ? 'environment' : 'user';
camVideo = camera == 'user' ? true : { facingMode: { exact: camera } };
// some devices can't swap the cam, if have Video Track already in execution.
await stopLocalVideoTrack();
try {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
const camStream = await navigator.mediaDevices.getUserMedia({ video: camVideo });
if (camStream) {
await refreshMyLocalStream(camStream);
await refreshMyStreamToPeers(camStream);
await setLocalMaxFps(videoMaxFrameRate);
await handleLocalCameraMirror();
await setMyVideoStatusTrue();
}
} catch (err) {
console.log('[Error] to swapping camera', err);
userLog('error', 'Error to swapping the camera ' + err);
// https://blog.addpipe.com/common-getusermedia-errors/
}
}
/**
* Stop Local Video Track
*/
async function stopLocalVideoTrack() {
if (useVideo || !isScreenStreaming) {
const localVideoTrack = localVideoMediaStream.getVideoTracks()[0];
if (localVideoTrack) {
console.log('stopLocalVideoTrack', localVideoTrack);
localVideoTrack.stop();
}
}
}
/**
* Stop Local Audio Track
*/
async function stopLocalAudioTrack() {
const localAudioTrack = localAudioMediaStream.getAudioTracks()[0];
if (localAudioTrack) {
console.log('stopLocalAudioTrack', localAudioTrack);
localAudioTrack.stop();
}
}
/**
* Toggle screen sharing and handle related actions
* @param {boolean} init - Indicates if it's the initial screen share state
*/
async function toggleScreenSharing(init = false) {
try {
// Set screen frame rate
screenMaxFrameRate = parseInt(screenFpsSelect.value, 10);
// Screen share constraints
const constraints = {
audio: myAudioStatus ? false : true,
video: { frameRate: screenMaxFrameRate },
};
// Store webcam video status before screen sharing
if (!isScreenStreaming) {
myVideoStatusBefore = myVideoStatus;
console.log('My video status before screen sharing: ' + myVideoStatusBefore);
} else {
if (!useVideo && !useAudio) {
return handleToggleScreenException('Audio and Video are disabled', init);
}
}
// Get screen or webcam media stream based on current state
const screenMediaPromise = isScreenStreaming
? await navigator.mediaDevices.getUserMedia(await getAudioVideoConstraints())
: await navigator.mediaDevices.getDisplayMedia(constraints);
if (screenMediaPromise) {
isVideoPrivacyActive = false;
emitPeerStatus('privacy', isVideoPrivacyActive);
isScreenStreaming = !isScreenStreaming;
myScreenStatus = isScreenStreaming;
if (isScreenStreaming) {
setMyVideoStatusTrue();
emitPeersAction('screenStart');
} else {
emitPeersAction('screenStop');
adaptAspectRatio();
// Reset zoom
myVideo.style.transform = '';
myVideo.style.transformOrigin = 'center';
}
await emitPeerStatus('screen', myScreenStatus);
await stopLocalVideoTrack();
await refreshMyLocalStream(screenMediaPromise, !useAudio);
await refreshMyStreamToPeers(screenMediaPromise, !useAudio);
if (init) {
// Handle init media stream
if (initStream) await stopTracks(initStream);
initStream = screenMediaPromise;
if (hasVideoTrack(initStream)) {
const newInitStream = new MediaStream([initStream.getVideoTracks()[0]]);
elemDisplay(initVideo, true, 'block');
initVideo.classList.toggle('mirror');
initVideo.srcObject = newInitStream;
disable(initVideoSelect, isScreenStreaming);
disable(initVideoBtn, isScreenStreaming);
} else {
elemDisplay(initVideo, false);
}
}
// Disable cam video when screen sharing stops
if (!init && !isScreenStreaming && !myVideoStatusBefore) setMyVideoOff(myPeerName);
// Enable cam video when screen sharing stops
if (!init && !isScreenStreaming && myVideoStatusBefore) setMyVideoStatusTrue();
myVideo.classList.toggle('mirror');
setScreenSharingStatus(isScreenStreaming);
if (myVideoAvatarImage && !useVideo) {
isScreenStreaming
? elemDisplay(myVideoAvatarImage, false)
: elemDisplay(myVideoAvatarImage, true, 'block');
}
if (myPrivacyBtn) {
isScreenStreaming ? elemDisplay(myPrivacyBtn, false) : elemDisplay(myPrivacyBtn, true);
}
if ((isScreenStreaming && thereArePeerConnections()) || isVideoPinned) {
myVideoPinBtn.click();
}
}
} catch (err) {
err.name === 'NotAllowedError'
? console.error('Screen sharing permission was denied by the user.')
: await handleToggleScreenException(`[Warning] Unable to share the screen: ${err}`, init);
if (init) return;
}
}
/**
* Handle exception and actions when toggling screen sharing
* @param {string} reason - The reason message
* @param {boolean} init - Indicates whether it's an initial state
*/
async function handleToggleScreenException(reason, init) {
try {
console.warn('handleToggleScreenException', reason);
// Update video privacy status
isVideoPrivacyActive = false;
emitPeerStatus('privacy', isVideoPrivacyActive);
// Inform peers about screen sharing stop
emitPeersAction('screenStop');
// Turn off your video
setMyVideoOff(myPeerName);
// Toggle screen streaming status
isScreenStreaming = !isScreenStreaming;
myScreenStatus = isScreenStreaming;
// Update screen sharing status
setScreenSharingStatus(isScreenStreaming);
// Emit screen status to peers
await emitPeerStatus('screen', myScreenStatus);
// Stop the local video track
await stopLocalVideoTrack();
// Handle video status based on conditions
if (!init && !isScreenStreaming && !myVideoStatusBefore) {
setMyVideoOff(myPeerName);
} else if (!init && !isScreenStreaming && myVideoStatusBefore) {
setMyVideoStatusTrue();
}
// Toggle the 'mirror' class on myVideo
myVideo.classList.toggle('mirror');
// Handle video avatar image and privacy button visibility
if (myVideoAvatarImage && !useVideo) {
isScreenStreaming ? elemDisplay(myVideoAvatarImage, false) : elemDisplay(myVideoAvatarImage, true, 'block');
}
// Automatically pin the video if screen sharing or video is pinned
if (isScreenStreaming || isVideoPinned) {
myVideoPinBtn.click();
}
} catch (error) {
console.error('[Error] An unexpected error occurred', error);
}
}
/**
* Set Screen Sharing Status
* @param {boolean} status of screen sharing
*/
function setScreenSharingStatus(status) {
if (!useVideo) {
status ? elemDisplay(myVideo, true, 'block') : elemDisplay(myVideo, false);
}
initScreenShareBtn.className = status ? className.screenOff : className.screenOn;
screenShareBtn.className = status ? className.screenOff : className.screenOn;
setTippy(screenShareBtn, status ? 'Stop screen sharing' : 'Start screen sharing', placement);
}
/**
* Set myVideoStatus true
*/
async function setMyVideoStatusTrue() {
if (myVideoStatus || !useVideo) return;
// Put video status already ON
localVideoMediaStream.getVideoTracks()[0].enabled = true;
myVideoStatus = true;
initVideoBtn.className = className.videoOn;
videoBtn.className = className.videoOn;
myVideoStatusIcon.className = className.videoOn;
elemDisplay(myVideoAvatarImage, false);
elemDisplay(myVideo, true, 'block');
setTippy(videoBtn, 'Stop the video', placement);
setTippy(initVideoBtn, 'Stop the video', 'top');
emitPeerStatus('video', myVideoStatus);
}
/**
* Enter - esc on full screen mode
* https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
*/
function toggleFullScreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
fullScreenBtn.className = className.fsOn;
isDocumentOnFullScreen = true;
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
fullScreenBtn.className = className.fsOff;
isDocumentOnFullScreen = false;
}
}
setTippy(fullScreenBtn, isDocumentOnFullScreen ? 'Exit full screen' : 'View full screen', placement);
}
/**
* Refresh my stream changes to connected peers in the room
* https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/replaceTrack
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getSenders
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack
*
* @param {MediaStream} stream - Media stream (audio/video) to refresh to peers.
* @param {boolean} localAudioTrackChange - Indicates whether there's a change in the local audio track (default false).
*/
async function refreshMyStreamToPeers(stream, localAudioTrackChange = false) {
if (!thereArePeerConnections()) return;
if (useAudio && localAudioTrackChange) localAudioMediaStream.getAudioTracks()[0].enabled = myAudioStatus;
// Log peer connections and all peers
console.log('PEER-CONNECTIONS', peerConnections);
console.log('ALL-PEERS', allPeers);
// Check if the passed stream has an audio track
const streamHasAudioTrack = hasAudioTrack(stream);
// Check if the passed stream has an video track
const streamHasVideoTrack = hasVideoTrack(stream);
// Check if the local stream has an audio track
const localStreamHasAudioTrack = hasAudioTrack(localAudioMediaStream);
// Check if the local stream has an video track
const localStreamHasVideoTrack = hasVideoTrack(localVideoMediaStream);
// Determine the audio stream to add to peers
const audioStream = streamHasAudioTrack ? stream : localStreamHasAudioTrack && localAudioMediaStream;
// Determine the audio track to replace to peers
const audioTrack =
streamHasAudioTrack && (localAudioTrackChange || isScreenStreaming)
? stream.getAudioTracks()[0]
: localStreamHasAudioTrack && localAudioMediaStream.getAudioTracks()[0];
// Determine the video stream to add to peers
const videoStream = streamHasVideoTrack ? stream : localStreamHasVideoTrack && localVideoMediaStream;
// Determine the video track to replace to peers
const videoTracks = streamHasVideoTrack
? stream.getVideoTracks()[0]
: localStreamHasVideoTrack && localVideoMediaStream.getVideoTracks()[0];
// Refresh my stream to connected peers except myself
for (const peer_id in peerConnections) {
const peer_name = allPeers[peer_id]['peer_name'];
// Replace video track
const videoSender = peerConnections[peer_id].getSenders().find((s) => s.track && s.track.kind === 'video');
if (useVideo && videoSender) {
videoSender.replaceTrack(videoTracks);
console.log('REPLACE VIDEO TRACK TO', { peer_id, peer_name, video: videoTracks });
} else {
if (videoStream) {
// Add video track if sender does not exist
videoStream.getTracks().forEach(async (track) => {
if (track.kind === 'video') {
peerConnections[peer_id].addTrack(track);
await handleRtcOffer(peer_id); // https://groups.google.com/g/discuss-webrtc/c/Ky3wf_hg1l8?pli=1
console.log('ADD VIDEO TRACK TO', { peer_id, peer_name, video: track });
}
});
}
}
// Replace audio track
const audioSender = peerConnections[peer_id].getSenders().find((s) => s.track && s.track.kind === 'audio');
if (audioSender && audioTrack) {
audioSender.replaceTrack(audioTrack);
console.log('REPLACE AUDIO TRACK TO', { peer_id, peer_name, audio: audioTrack });
} else {
if (audioStream) {
// Add audio track if sender does not exist
audioStream.getTracks().forEach(async (track) => {
if (track.kind === 'audio') {
peerConnections[peer_id].addTrack(track);
await handleRtcOffer(peer_id); // https://groups.google.com/g/discuss-webrtc/c/Ky3wf_hg1l8?pli=1
console.log('ADD AUDIO TRACK TO', { peer_id, peer_name, audio: track });
}
});
}
}
}
}
/**
* Refresh my local stream
* @param {object} stream media stream audio - video
* @param {boolean} localAudioTrackChange default false
*/
async function refreshMyLocalStream(stream, localAudioTrackChange = false) {
// enable video
if (useVideo || isScreenStreaming) stream.getVideoTracks()[0].enabled = true;
const tracksToInclude = [];
const videoTrack = hasVideoTrack(stream)
? stream.getVideoTracks()[0]
: hasVideoTrack(localVideoMediaStream) && localVideoMediaStream.getVideoTracks()[0];
const audioTrack =
hasAudioTrack(stream) && localAudioTrackChange
? stream.getAudioTracks()[0]
: hasAudioTrack(localAudioMediaStream) && localAudioMediaStream.getAudioTracks()[0];
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream
if (useVideo || isScreenStreaming) {
console.log('Refresh my local media stream VIDEO - AUDIO', { isScreenStreaming: isScreenStreaming });
if (videoTrack) {
tracksToInclude.push(videoTrack);
localVideoMediaStream = new MediaStream([videoTrack]);
attachMediaStream(myVideo, localVideoMediaStream);
logStreamSettingsInfo('refreshMyLocalStream-localVideoMediaStream', localVideoMediaStream);
}
if (audioTrack) {
tracksToInclude.push(audioTrack);
localAudioMediaStream = new MediaStream([audioTrack]);
attachMediaStream(myAudio, localAudioMediaStream);
getMicrophoneVolumeIndicator(localAudioMediaStream);
logStreamSettingsInfo('refreshMyLocalStream-localAudioMediaStream', localAudioMediaStream);
}
} else {
console.log('Refresh my local media stream AUDIO');
if (useAudio && audioTrack) {
tracksToInclude.push(audioTrack);
localAudioMediaStream = new MediaStream([audioTrack]);
getMicrophoneVolumeIndicator(localAudioMediaStream);
logStreamSettingsInfo('refreshMyLocalStream-localAudioMediaStream', localAudioMediaStream);
}
}
if (isScreenStreaming) {
// refresh video privacy mode on screen sharing
isVideoPrivacyActive = false;
setVideoPrivacyStatus('myVideo', isVideoPrivacyActive);
// on toggleScreenSharing video stop from popup bar
stream.getVideoTracks()[0].onended = () => {
toggleScreenSharing();
};
}
// adapt video object fit on screen streaming
myVideo.style.objectFit = isScreenStreaming ? 'contain' : 'var(--video-object-fit)';
}
/**
* Check if MediaStream has audio track
* @param {MediaStream} mediaStream
* @returns boolean
*/
function hasAudioTrack(mediaStream) {
if (!mediaStream) return false;
const audioTracks = mediaStream.getAudioTracks();
return audioTracks.length > 0;
}
/**
* Check if MediaStream has video track
* @param {MediaStream} mediaStream
* @returns boolean
*/
function hasVideoTrack(mediaStream) {
if (!mediaStream) return false;
const videoTracks = mediaStream.getVideoTracks();
return videoTracks.length > 0;
}
/**
* Check if recording is active, if yes,
* on disconnect, remove peer, kick out or leave room, we going to save it
*/
function checkRecording() {
if (isStreamRecording || myVideoParagraph.innerText.includes('REC')) {
console.log('Going to save recording');
stopStreamRecording();
}
}
/**
* Handle recording errors
* @param {string} error
*/
function handleRecordingError(error, popupLog = true) {
console.error('Recording error', error);
if (popupLog) userLog('error', error);
}
/**
* Seconds to HMS
* @param {number} d
* @return {string} format HH:MM:SS
*/
function secondsToHms(d) {
d = Number(d);
const h = Math.floor(d / 3600);
const m = Math.floor((d % 3600) / 60);
const s = Math.floor((d % 3600) % 60);
const hDisplay = h > 0 ? h + 'h' : '';
const mDisplay = m > 0 ? m + 'm' : '';
const sDisplay = s > 0 ? s + 's' : '';
return hDisplay + ' ' + mDisplay + ' ' + sDisplay;
}
/**
* Start/Stop recording timer
*/
function startRecordingTimer() {
resumeRecButtons();
recElapsedTime = 0;
recTimer = setInterval(function printTime() {
if (!isStreamRecordingPaused) {
recElapsedTime++;
let recTimeElapsed = secondsToHms(recElapsedTime);
myVideoParagraph.innerText = myPeerName + ' 🔴 REC ' + recTimeElapsed;
recordingTime.innerText = recTimeElapsed;
}
}, 1000);
}
function stopRecordingTimer() {
clearInterval(recTimer);
resetRecButtons();
}
/**
* Get MediaRecorder MimeTypes
* @returns {boolean} is mimeType supported by media recorder
*/
function 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);
});
}
/**
* Start Recording
* https://github.com/webrtc/samples/tree/gh-pages/src/content/getusermedia/record
* https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder
* https://developer.mozilla.org/en-US/docs/Web/API/MediaStream
*/
function startStreamRecording() {
recordedBlobs = [];
// Get supported MIME types and set options
const supportedMimeTypes = getSupportedMimeTypes();
console.log('MediaRecorder supported options', supportedMimeTypes);
const options = { mimeType: supportedMimeTypes[0] };
recCodecs = supportedMimeTypes[0];
try {
audioRecorder = new MixedAudioRecorder();
const audioStreams = getAudioStreamFromAudioElements();
console.log('Audio streams tracks --->', audioStreams.getTracks());
const audioMixerStreams = audioRecorder.getMixedAudioStream(
audioStreams
.getTracks()
.filter((track) => track.kind === 'audio')
.map((track) => new MediaStream([track]))
);
const audioMixerTracks = audioMixerStreams.getTracks();
console.log('Audio mixer tracks --->', audioMixerTracks);
isMobileDevice ? startMobileRecording(options, audioMixerTracks) : recordingOptions(options, audioMixerTracks);
} catch (err) {
handleRecordingError('Exception while creating MediaRecorder: ' + err);
}
}
/**
* Recording options Camera or Screen/Window for Desktop devices
* @param {MediaRecorderOptions} options - MediaRecorder options.
* @param {array} audioMixerTracks - Array of audio tracks from the audio mixer.
*/
function recordingOptions(options, audioMixerTracks) {
Swal.fire({
background: swBg,
position: 'top',
imageUrl: images.recording,
title: 'Recording options',
text: 'Select the recording type you want to start. Audio will be recorded from all participants.',
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) {
startMobileRecording(options, audioMixerTracks);
} else if (result.isDenied) {
startDesktopRecording(options, audioMixerTracks);
}
});
}
/**
* Starts mobile recording with the specified options and audio mixer tracks.
* @param {MediaRecorderOptions} options - MediaRecorder options.
* @param {array} audioMixerTracks - Array of audio tracks from the audio mixer.
*/
function startMobileRecording(options, audioMixerTracks) {
try {
// Combine audioMixerTracks and videoTracks into a single array
const combinedTracks = [];
// Add audio mixer tracks to the combinedTracks array if available
if (Array.isArray(audioMixerTracks)) {
combinedTracks.push(...audioMixerTracks);
}
// Check if there's a local media stream (presumably for the camera)
if (useVideo && localVideoMediaStream !== null) {
const videoTracks = localVideoMediaStream.getVideoTracks();
console.log('Cam video tracks --->', videoTracks);
// Add video tracks from the local media stream to combinedTracks if available
if (Array.isArray(videoTracks)) {
combinedTracks.push(...videoTracks);
}
}
// Create a new MediaStream using the combinedTracks
const recCamStream = new MediaStream(combinedTracks);
console.log('New Cam Media Stream tracks --->', recCamStream.getTracks());
// Create a MediaRecorder instance with the combined stream and specified options
mediaRecorder = new MediaRecorder(recCamStream, options);
console.log('Created MediaRecorder', mediaRecorder, 'with options', options);
// Call a function to handle the MediaRecorder
handleMediaRecorder(mediaRecorder);
} catch (err) {
// Handle any errors that occur during the recording setup
handleRecordingError('Unable to record the camera + audio: ' + err, false);
}
}
/**
* Starts desktop recording with the specified options and audio mixer tracks.
* On desktop devices, it records the screen or window along with all audio tracks.
* @param {MediaRecorderOptions} options - MediaRecorder options.
* @param {array} audioMixerTracks - Array of audio tracks from the audio mixer.
*/
function startDesktopRecording(options, audioMixerTracks) {
// Get the desired frame rate for screen recording
// screenMaxFrameRate = parseInt(screenFpsSelect.value, 10);
// Define constraints for capturing the screen
const constraints = {
video: { frameRate: { max: 30 } }, // Recording max 30fps
};
// Request access to screen capture using the specified constraints
navigator.mediaDevices
.getDisplayMedia(constraints)
.then((screenStream) => {
// Get video tracks from the screen capture stream
const screenTracks = screenStream.getVideoTracks();
console.log('Screen video tracks --->', screenTracks);
// Create an array to combine screen tracks and audio mixer tracks
const combinedTracks = [];
// Add screen video tracks to combinedTracks if available
if (Array.isArray(screenTracks)) {
combinedTracks.push(...screenTracks);
}
// Add audio mixer tracks to combinedTracks if available
if (useAudio && Array.isArray(audioMixerTracks)) {
combinedTracks.push(...audioMixerTracks);
}
// Create a new MediaStream using the combinedTracks
recScreenStream = new MediaStream(combinedTracks);
console.log('New Screen/Window Media Stream tracks --->', recScreenStream.getTracks());
// Create a MediaRecorder instance with the combined stream and specified options
mediaRecorder = new MediaRecorder(recScreenStream, options);
console.log('Created MediaRecorder', mediaRecorder, 'with options', options);
// Set a flag to indicate that screen recording is active
isRecScreenStream = true;
// Call a function to handle the MediaRecorder
handleMediaRecorder(mediaRecorder);
})
.catch((err) => {
// Handle any errors that occur during screen recording setup
handleRecordingError('Unable to record the screen + audio: ' + err, false);
});
}
/**
* Get a MediaStream containing audio tracks from audio elements on the page.
* @returns {MediaStream} A MediaStream containing audio tracks.
*/
function getAudioStreamFromAudioElements() {
const audioElements = getSlALL('audio');
const audioStream = new MediaStream();
audioElements.forEach((audio) => {
const audioTrack = audio.srcObject.getAudioTracks()[0];
if (audioTrack) {
audioStream.addTrack(audioTrack);
}
});
return audioStream;
}
/**
* Notify me if someone start to recording they camera/screen/window + audio
* @param {string} fromId peer_id
* @param {string} from peer_name
* @param {string} fromAvatar peer_avatar
* @param {string} action recording action
*/
function notifyRecording(fromId, from, fromAvatar, action) {
const msg = '🔴 ' + action + ' conference recording';
const chatMessage = {
from: from,
fromAvatar: fromAvatar,
fromId: fromId,
to: myPeerName,
msg: msg,
privateMsg: false,
};
handleDataChannelChat(chatMessage);
if (!showChatOnMessage) {
const recAgree = action != 'Stop' ? 'Your presence implies you agree to being recorded' : '';
toastMessage(
null,
null,
`${from}
<br /><br />
<span>${msg}</span>
<br /><br />
${recAgree}`,
'top-end',
6000
);
}
}
/**
* Toggle Video and Audio tabs
* @param {boolean} disabled - If true, disable the tabs; otherwise, enable them
*/
function toggleVideoAudioTabs(disabled = false) {
if (disabled) tabRoomBtn.click();
tabVideoBtn.disabled = disabled;
tabAudioBtn.disabled = disabled;
}
/**
* Handle Media Recorder
* @param {object} mediaRecorder
*/
function handleMediaRecorder(mediaRecorder) {
mediaRecorder.start();
mediaRecorder.addEventListener('start', handleMediaRecorderStart);
mediaRecorder.addEventListener('dataavailable', handleMediaRecorderData);
mediaRecorder.addEventListener('stop', handleMediaRecorderStop);
}
/**
* Handle Media Recorder onstart event
* @param {object} event of media recorder
*/
function handleMediaRecorderStart(event) {
toggleVideoAudioTabs(true);
startRecordingTimer();
emitPeersAction('recStart');
emitPeerStatus('rec', true);
console.log('MediaRecorder started: ', event);
isStreamRecording = true;
recordStreamBtn.style.setProperty('color', '#ff4500');
setTippy(recordStreamBtn, 'Stop recording', placement);
if (isMobileDevice) elemDisplay(swapCameraBtn, false);
playSound('recStart');
}
/**
* Handle Media Recorder ondata event
* @param {object} event of media recorder
*/
function handleMediaRecorderData(event) {
console.log('MediaRecorder data: ', event);
if (event.data && event.data.size > 0) recordedBlobs.push(event.data);
}
/**
* Handle Media Recorder onstop event
* @param {object} event of media recorder
*/
function handleMediaRecorderStop(event) {
toggleVideoAudioTabs(false);
console.log('MediaRecorder stopped: ', event);
console.log('MediaRecorder Blobs: ', recordedBlobs);
stopRecordingTimer();
emitPeersAction('recStop');
emitPeerStatus('rec', false);
isStreamRecording = false;
myVideoParagraph.innerText = myPeerName + ' (me)';
if (isRecScreenStream) {
recScreenStream.getTracks().forEach((track) => {
if (track.kind === 'video') track.stop();
});
isRecScreenStream = false;
}
recordStreamBtn.style.setProperty('color', '#ffffff');
downloadRecordedStream();
setTippy(recordStreamBtn, 'Start recording', placement);
if (isMobileDevice) elemDisplay(swapCameraBtn, true, 'block');
playSound('recStop');
}
/**
* Stop recording
*/
function stopStreamRecording() {
mediaRecorder.stop();
audioRecorder.stopMixedAudioStream();
}
/**
* Pause recording display buttons
*/
function pauseRecButtons() {
elemDisplay(pauseRecBtn, false);
elemDisplay(resumeRecBtn, true);
}
/**
* Resume recording display buttons
*/
function resumeRecButtons() {
elemDisplay(resumeRecBtn, false);
elemDisplay(pauseRecBtn, true);
}
/**
* Reset recording display buttons
*/
function resetRecButtons() {
elemDisplay(pauseRecBtn, false);
elemDisplay(resumeRecBtn, false);
}
/**
* Pause recording
*/
function pauseRecording() {
if (mediaRecorder) {
isStreamRecordingPaused = true;
mediaRecorder.pause();
pauseRecButtons();
console.log('Pause recording');
}
}
/**
* Resume recording
*/
function resumeRecording() {
if (mediaRecorder) {
mediaRecorder.resume();
isStreamRecordingPaused = false;
resumeRecButtons();
console.log('Resume recording');
}
}
/**
* Download recorded stream
*/
function downloadRecordedStream() {
try {
const type = recordedBlobs[0].type.includes('mp4') ? 'mp4' : 'webm';
const blob = new Blob(recordedBlobs, { type: 'video/' + type });
const recFileName = getDataTimeString() + '-REC.' + type;
const currentDevice = isMobileDevice ? 'MOBILE' : 'PC';
const blobFileSize = bytesToSize(blob.size);
const recordingInfo = `
<br/>
<br/>
<ul>
<li>Time: ${recordingTime.innerText}</li>
<li>File: ${recFileName}</li>
<li>Codecs: ${recCodecs}</li>
<li>Size: ${blobFileSize}</li>
</ul>
<br/>
`;
lastRecordingInfo.innerHTML = `<br/>Last recording info: ${recordingInfo}`;
recordingTime.innerText = '';
userLog(
'success-html',
`<div style="text-align: left;">
🔴 &nbsp; Recording Info: <br/>
${recordingInfo}
Please wait to be processed, then will be downloaded to your ${currentDevice} device.
</div>`
);
saveBlobToFile(blob, recFileName);
} catch (err) {
userLog('error', 'Recording save failed: ' + err);
}
}
/**
* Create Chat Room Data Channel
* @param {string} peer_id socket.id
*/
function createChatDataChannel(peer_id) {
chatDataChannels[peer_id] = peerConnections[peer_id].createDataChannel('mirotalk_chat_channel');
chatDataChannels[peer_id].onopen = (event) => {
console.log('chatDataChannels created', event);
};
}
/**
* Set the chat room & caption on full screen mode for mobile
*/
function setChatRoomAndCaptionForMobile() {
if (isMobileDevice) {
// chat full screen
setSP('--msger-height', '99%');
setSP('--msger-width', '99%');
// caption full screen
setSP('--caption-height', '99%');
setSP('--caption-width', '99%');
} else {
// make chat room draggable for desktop
dragElement(msgerDraggable, msgerHeader);
// make caption draggable for desktop
dragElement(captionDraggable, captionHeader);
}
}
/**
* Show msger draggable on center screen position
*/
function showChatRoomDraggable() {
playSound('newMessage');
if (isMobileDevice) {
elemDisplay(buttonsBar, false);
elemDisplay(bottomButtons, false);
isButtonsVisible = false;
}
//chatLeftCenter();
chatCenter();
chatRoomBtn.className = className.chatOff;
isChatRoomVisible = true;
if (isDesktopDevice && canBePinned()) {
toggleChatPin();
}
setTippy(chatRoomBtn, 'Close the chat', bottomButtonsPlacement);
}
/**
* Show caption box draggable on center screen position
*/
function showCaptionDraggable() {
playSound('newMessage');
if (isMobileDevice) {
elemDisplay(buttonsBar, false);
elemDisplay(bottomButtons, false);
isButtonsVisible = false;
}
//captionRightCenter();
captionCenter();
captionBtn.className = 'far fa-closed-captioning';
isCaptionBoxVisible = true;
if (isDesktopDevice && canBePinned()) {
toggleCaptionPin();
}
setTippy(captionBtn, 'Close the caption', placement);
}
/**
* Toggle Chat dropdown menu
*/
function toggleChatDropDownMenu() {
msgerDropDownContent.style.display === 'block'
? (msgerDropDownContent.style.display = 'none')
: (msgerDropDownContent.style.display = 'block');
}
/**
* Chat maximize
*/
function chatMaximize() {
elemDisplay(msgerMaxBtn, false);
elemDisplay(msgerMinBtn, true);
chatCenter();
setSP('--msger-width', '100%');
setSP('--msger-height', '100%');
}
/**
* Chat minimize
*/
function chatMinimize() {
elemDisplay(msgerMinBtn, false);
elemDisplay(msgerMaxBtn, true);
chatCenter();
if (!isChatPinned) {
if (isMobileDevice) {
setSP('--msger-width', '99%');
setSP('--msger-height', '99%');
} else {
setSP('--msger-width', '420px');
setSP('--msger-height', '680px');
}
} else {
setSP('--msger-width', '25%');
setSP('--msger-height', '100%');
}
}
/**
* Set chat position
*/
function chatCenter() {
if (!isChatPinned) {
msgerDraggable.style.position = 'fixed';
msgerDraggable.style.display = 'flex';
msgerDraggable.style.top = '50%';
msgerDraggable.style.left = '50%';
msgerDraggable.style.transform = 'translate(-50%, -50%)';
msgerDraggable.style.webkitTransform = 'translate(-50%, -50%)';
msgerDraggable.style.mozTransform = 'translate(-50%, -50%)';
}
}
/**
* Check if the element can be pinned based of viewport size
* @returns boolean
*/
function canBePinned() {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
return viewportWidth >= 1024 && viewportHeight >= 768;
}
/**
* Toggle Chat Pin
*/
function toggleChatPin() {
if (isCaptionPinned) {
return userLog('toast', 'Please unpin the Caption that appears to be currently pinned');
}
isChatPinned ? chatUnpin() : chatPin();
playSound('click');
}
/**
* Handle chat pin
*/
function chatPin() {
videoMediaContainerPin();
chatPinned();
isChatPinned = true;
setColor(msgerTogglePin, 'lime');
resizeVideoMedia();
msgerDraggable.style.resize = 'none';
if (!isMobileDevice) undragElement(msgerDraggable, msgerHeader);
}
/**
* Handle chat unpin
*/
function chatUnpin() {
videoMediaContainerUnpin();
setSP('--msger-width', '420px');
setSP('--msger-height', '680px');
elemDisplay(msgerMinBtn, false);
buttons.chat.showMaxBtn && elemDisplay(msgerMaxBtn, true);
isChatPinned = false;
//chatLeftCenter();
chatCenter();
setColor(msgerTogglePin, 'white');
resizeVideoMedia();
msgerDraggable.style.resize = 'both';
if (!isMobileDevice) dragElement(msgerDraggable, msgerHeader);
}
/**
* Move Chat center left
*/
function chatLeftCenter() {
msgerDraggable.style.position = 'fixed';
msgerDraggable.style.display = 'flex';
msgerDraggable.style.top = '50%';
msgerDraggable.style.left = isMobileDevice ? '50%' : '25%';
msgerDraggable.style.transform = 'translate(-50%, -50%)';
}
/**
* Chat is pinned
*/
function chatPinned() {
msgerDraggable.style.position = 'absolute';
msgerDraggable.style.top = 0;
msgerDraggable.style.right = 0;
msgerDraggable.style.left = null;
msgerDraggable.style.transform = null;
setSP('--msger-width', '25%');
setSP('--msger-height', '100%');
}
/**
* Caption maximize
*/
function captionMaximize() {
elemDisplay(captionMaxBtn, false);
elemDisplay(captionMinBtn, true);
captionCenter();
setSP('--caption-width', '100%');
setSP('--caption-height', '100%');
}
/**
* Caption minimize
*/
function captionMinimize() {
elemDisplay(captionMinBtn, false);
elemDisplay(captionMaxBtn, true);
captionCenter();
if (!isCaptionPinned) {
if (isMobileDevice) {
setSP('--caption-width', '99%');
setSP('--caption-height', '99%');
} else {
setSP('--caption-width', '420px');
setSP('--caption-height', '680px');
}
} else {
setSP('--caption-width', '25%');
setSP('--caption-height', '100%');
}
}
/**
* Set chat position
*/
function captionCenter() {
if (!isCaptionPinned) {
captionDraggable.style.position = 'fixed';
captionDraggable.style.display = 'flex';
captionDraggable.style.top = '50%';
captionDraggable.style.left = '50%';
captionDraggable.style.transform = 'translate(-50%, -50%)';
captionDraggable.style.webkitTransform = 'translate(-50%, -50%)';
captionDraggable.style.mozTransform = 'translate(-50%, -50%)';
}
}
/**
* Toggle Caption Pin
*/
function toggleCaptionPin() {
if (isChatPinned) {
return userLog('toast', 'Please unpin the Chat that appears to be currently pinned');
}
isCaptionPinned ? captionUnpin() : captionPin();
playSound('click');
}
/**
* Handle caption pin
*/
function captionPin() {
videoMediaContainerPin();
captionPinned();
isCaptionPinned = true;
setColor(captionTogglePin, 'lime');
resizeVideoMedia();
captionDraggable.style.resize = 'none';
if (!isMobileDevice) undragElement(captionDraggable, captionHeader);
}
/**
* Handle caption unpin
*/
function captionUnpin() {
videoMediaContainerUnpin();
setSP('--caption-width', '420px');
setSP('--caption-height', '680px');
elemDisplay(captionMinBtn, false);
buttons.caption.showMaxBtn && elemDisplay(captionMaxBtn, true);
isCaptionPinned = false;
//captionRightCenter();
captionCenter();
setColor(captionTogglePin, 'white');
resizeVideoMedia();
captionDraggable.style.resize = 'both';
if (!isMobileDevice) dragElement(captionDraggable, captionHeader);
}
/**
* Move Caption center right
*/
function captionRightCenter() {
captionDraggable.style.position = 'fixed';
captionDraggable.style.display = 'flex';
captionDraggable.style.top = '50%';
captionDraggable.style.left = isMobileDevice ? '50%' : '75%';
captionDraggable.style.transform = 'translate(-50%, -50%)';
}
/**
* Caption is pinned
*/
function captionPinned() {
captionDraggable.style.position = 'absolute';
captionDraggable.style.top = 0;
captionDraggable.style.right = 0;
captionDraggable.style.left = null;
captionDraggable.style.transform = null;
setSP('--caption-width', '25%');
setSP('--caption-height', '100%');
}
/**
* Clean chat messages
*/
function cleanMessages() {
playSound('newMessage');
Swal.fire({
background: swBg,
position: 'top',
title: 'Chat',
text: 'Clean up chat messages?',
imageUrl: images.delete,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
// clean chat messages
if (result.isConfirmed) {
let msgs = msgerChat.firstChild;
while (msgs) {
msgerChat.removeChild(msgs);
msgs = msgerChat.firstChild;
}
// clean chat messages
chatMessages = [];
// clean chatGPT context
chatGPTcontext = [];
playSound('delete');
}
});
}
/**
* Clean captions
*/
function cleanCaptions() {
playSound('newMessage');
Swal.fire({
background: swBg,
position: 'center',
title: 'Clean up all caption transcripts?',
imageUrl: images.delete,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
// clean chat messages
if (result.isConfirmed) {
let captions = captionChat.firstChild;
while (captions) {
captionChat.removeChild(captions);
captions = captionChat.firstChild;
}
// clean object
transcripts = [];
playSound('delete');
}
});
}
/**
* Hide chat room and emoji picker
*/
function hideChatRoomAndEmojiPicker() {
if (isChatPinned) {
chatUnpin();
}
elemDisplay(msgerDraggable, false);
elemDisplay(msgerEmojiPicker, false);
setColor(msgerEmojiBtn, '#FFFFFF');
chatRoomBtn.className = className.chatOn;
isChatRoomVisible = false;
isChatEmojiVisible = false;
setTippy(chatRoomBtn, 'Open the chat', bottomButtonsPlacement);
}
/**
* Hide chat room and emoji picker
*/
function hideCaptionBox() {
if (isCaptionPinned) {
captionUnpin();
}
elemDisplay(captionDraggable, false);
captionBtn.className = className.captionOn;
isCaptionBoxVisible = false;
setTippy(captionBtn, 'Open the caption', placement);
}
/**
* Send Chat messages to peers in the room
*/
async function sendChatMessage() {
if (!thereArePeerConnections() && !isChatGPTOn) {
cleanMessageInput();
isChatPasteTxt = false;
return userLog('info', "Can't send message, no participants in the room");
}
msgerInput.value = filterXSS(msgerInput.value.trim());
const msg = checkMsg(msgerInput.value);
// empty msg or
if (!msg) {
isChatPasteTxt = false;
return cleanMessageInput();
}
isChatGPTOn ? await getChatGPTmessage(msg) : emitMsg(myPeerName, myPeerAvatar, 'toAll', msg, false, myPeerId);
appendMessage(myPeerName, rightChatAvatar, 'right', msg, false);
cleanMessageInput();
}
/**
* handle Incoming Data Channel Chat Messages
* @param {object} dataMessage chat messages
*/
function handleDataChannelChat(dataMessage) {
if (!dataMessage) return;
// sanitize all params
const msgFrom = filterXSS(dataMessage.from);
const msgFromAvatar = filterXSS(dataMessage.fromAvatar);
const msgFromId = filterXSS(dataMessage.fromId);
const msgTo = filterXSS(dataMessage.to);
const msg = filterXSS(dataMessage.msg);
const msgPrivate = filterXSS(dataMessage.privateMsg);
const msgId = filterXSS(dataMessage.id);
// We check if the message is from real peer
const from_peer_name = allPeers[msgFromId]['peer_name'];
if (from_peer_name != msgFrom) {
console.log('Fake message detected', { realFrom: from_peer_name, fakeFrom: msgFrom, msg: msg });
return;
}
// private message but not for me return
if (msgPrivate && msgTo != myPeerName) return;
console.log('handleDataChannelChat', dataMessage);
// chat message for me also
if (!isChatRoomVisible && showChatOnMessage) {
showChatRoomDraggable();
chatRoomBtn.className = className.chatOff;
}
// show message from
if (!showChatOnMessage) {
userLog('toast', `New message from: ${msgFrom}`);
}
setPeerChatAvatarImgName('left', msgFrom, msgFromAvatar);
appendMessage(msgFrom, leftChatAvatar, 'left', msg, msgPrivate, msgId, msgFrom);
speechInMessages ? speechMessage(true, msgFrom, msg) : playSound('chatMessage');
}
/**
* Clean input txt message
*/
function cleanMessageInput() {
msgerInput.value = '';
msgerInput.style.height = '15px';
}
/**
* Paste from clipboard to input txt message
*/
function pasteToMessageInput() {
navigator.clipboard
.readText()
.then((text) => {
msgerInput.value += text;
isChatPasteTxt = true;
checkLineBreaks();
})
.catch((err) => {
console.error('Failed to read clipboard contents: ', err);
});
}
/**
* Handle text transcript getting from peers
* @param {object} config data
*/
function handleDataChannelSpeechTranscript(config) {
handleSpeechTranscript(config);
}
/**
* Handle text transcript getting from peers
* @param {object} config data
*/
function handleSpeechTranscript(config) {
if (!config) return;
console.log('Handle speech transcript', config);
config.text_data = filterXSS(config.text_data);
config.peer_name = filterXSS(config.peer_name);
config.peer_avatar = filterXSS(config.peer_avatar);
const { peer_name, peer_avatar, text_data } = config;
const time_stamp = getFormatDate(new Date());
const avatar_image =
peer_avatar && isImageURL(peer_avatar)
? peer_avatar
: isValidEmail(peer_name)
? genGravatar(peer_name)
: genAvatarSvg(peer_name, 32);
if (!isCaptionBoxVisible) showCaptionDraggable();
const msgHTML = `
<div class="msg left-msg">
<img class="msg-img" src="${avatar_image}" />
<div class="msg-caption-bubble">
<div class="msg-info">
<div class="msg-info-name">${peer_name} : ${time_stamp}</div>
</div>
<div class="msg-text">${text_data}</div>
</div>
</div>
`;
captionChat.insertAdjacentHTML('beforeend', msgHTML);
captionChat.scrollTop += 500;
transcripts.push({
time: time_stamp,
name: peer_name,
caption: text_data,
});
playSound('speech');
}
/**
* Escape Special Chars
* @param {string} regex string to replace
*/
function escapeSpecialChars(regex) {
return regex.replace(/([()[{*+.$^\\|?])/g, '\\$1');
}
/**
* Append Message to msger chat room
* @param {string} from peer name
* @param {string} img images url
* @param {string} side left/right
* @param {string} msg message to append
* @param {boolean} privateMsg if is private message
* @param {string} msgId peer id
* @param {string} to peer name
*/
function appendMessage(from, img, side, msg, privateMsg, msgId = null, to = '') {
let time = getFormatDate(new Date());
// sanitize all params
const getFrom = filterXSS(from);
const getTo = filterXSS(to);
const getSide = filterXSS(side);
const getImg = isChatGPTOn && getSide === 'left' ? images.chatgpt : filterXSS(img);
const getMsg = filterXSS(msg);
const getPrivateMsg = filterXSS(privateMsg);
const getMsgId = filterXSS(msgId);
const isChatGPT = getFrom === 'ChatGPT';
// collect chat messages to save it later
chatMessages.push({
time: time,
from: getFrom,
msg: getMsg,
privateMsg: getPrivateMsg,
});
// check if i receive a private message
let msgBubble = getPrivateMsg ? 'private-msg-bubble' : 'msg-bubble';
const isValidPrivateMessage = getPrivateMsg && getMsgId != null && getMsgId != myPeerId;
let msgHTML = `
<div id="msg-${chatMessagesId}" class="msg ${getSide}-msg">
<img class="msg-img" src="${getImg}" />
<div class=${msgBubble}>
<div class="msg-info">
<div class="msg-info-name">${getFrom}</div>
<div class="msg-info-time">${time}</div>
</div>
<div class="msg-text">
<span id="message-${chatMessagesId}"></span>
<hr/>
`;
// add btn direct reply to private message
if (isValidPrivateMessage) {
const privateMessageTag =
getSide === 'left' ? `Private message from ${getFrom}` : `Private message to ${getTo}`;
msgHTML += `<p class="b-yellow">${privateMessageTag}</p>`;
if (!isChatGPT && getSide === 'left') {
msgHTML += `
<button
class="${className.msgPrivate} b-yellow"
id="msg-private-reply-${chatMessagesId}"
style="color:#fff; border:none; background:transparent;"
onclick="sendPrivateMsgToPeer('${myPeerId}','${getFrom}')"
></button>`;
}
}
msgHTML += `
<button
id="msg-delete-${chatMessagesId}"
class="${className.trash}"
style="color:#fff; border:none; background:transparent;"
onclick="deleteMessage('msg-${chatMessagesId}')"
></button>
<button
id="msg-copy-${chatMessagesId}"
class="${className.copy}"
style="color:#fff; border:none; background:transparent;"
onclick="copyToClipboard('message-${chatMessagesId}')"
></button>`;
if (isSpeechSynthesisSupported) {
msgHTML += `
<button
id="msg-speech-${chatMessagesId}"
class="${className.speech}"
style="color:#fff; border:none; background:transparent;"
onclick="speechElementText(false, '${getFrom}', 'message-${chatMessagesId}')"
></button>`;
}
msgHTML += `
</div>
</div>
</div>
`;
msgerChat.insertAdjacentHTML('beforeend', msgHTML);
const message = getId(`message-${chatMessagesId}`);
if (message) {
if (getFrom === 'ChatGPT') {
// Stream the message for ChatGPT
streamMessage(message, getMsg, 100);
} else {
// Process the message for other senders
message.innerHTML = processMessage(getMsg);
hljs.highlightAll();
}
}
msgerChat.scrollTop += 500;
if (!isMobileDevice) {
setTippy(getId('msg-delete-' + chatMessagesId), 'Delete', 'top');
setTippy(getId('msg-copy-' + chatMessagesId), 'Copy', 'top');
setTippy(getId('msg-speech-' + chatMessagesId), 'Speech', 'top');
if (isValidPrivateMessage) {
setTippy(getId('msg-private-reply-' + chatMessagesId), 'Reply to ' + getTo, 'top');
}
}
chatMessagesId++;
}
/**
* Process Messages
* @param {string} message
* @returns string message processed
*/
function processMessage(message) {
const codeBlockRegex = /```([a-zA-Z0-9]+)?\n([\s\S]*?)```/g;
let parts = [];
let lastIndex = 0;
message.replace(codeBlockRegex, (match, lang, code, offset) => {
if (offset > lastIndex) {
parts.push({ type: 'text', value: message.slice(lastIndex, offset) });
}
parts.push({ type: 'code', lang, value: code });
lastIndex = offset + match.length;
});
if (lastIndex < message.length) {
parts.push({ type: 'text', value: message.slice(lastIndex) });
}
return parts
.map((part) => {
if (part.type === 'text') {
return part.value;
} else if (part.type === 'code') {
return `<pre><code class="language-${part.lang || ''}">${part.value}</code></pre>`;
}
})
.join('');
}
/**
* Stream message
* @param {string} element
* @param {string} message
* @param {integer} speed
*/
function streamMessage(element, message, speed = 100) {
const parts = processMessage(message);
const words = parts.split(' ');
let textBuffer = '';
let wordIndex = 0;
const interval = setInterval(() => {
if (wordIndex < words.length) {
textBuffer += words[wordIndex] + ' ';
element.innerHTML = textBuffer;
wordIndex++;
} else {
clearInterval(interval);
highlightCodeBlocks(element);
}
}, speed);
function highlightCodeBlocks(element) {
const codeBlocks = element.querySelectorAll('pre code');
codeBlocks.forEach((block) => {
hljs.highlightElement(block);
});
}
}
/**
* Speech message
* https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance
*
* @param {boolean} newMsg true/false
* @param {string} from peer_name
* @param {string} msg message
*/
function 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);
}
/**
* Speech element text
* @param {boolean} newMsg true/false
* @param {string} from peer_name
* @param {string} elemId
*/
function speechElementText(newMsg = true, from, elemId) {
const element = getId(elemId);
speechMessage(newMsg, from, element.innerText);
}
/**
* Delete message
* @param {string} id msg id
*/
function deleteMessage(id) {
playSound('newMessage');
Swal.fire({
background: swBg,
position: 'top',
title: 'Chat',
text: 'Delete this messages?',
imageUrl: images.delete,
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
// clean this message
if (result.isConfirmed) {
getId(id).remove();
playSound('delete');
}
});
}
/**
* Copy the element innerText on clipboard
* @param {string} id
*/
function copyToClipboard(id) {
const text = getId(id).innerText;
navigator.clipboard
.writeText(text)
.then(() => {
msgPopup('success', 'Message copied!', 'top-end', 1000);
})
.catch((err) => {
msgPopup('error', err, 'top', 2000);
});
}
/**
* Add participants in the chat room lists
* @param {object} peers all peers info connected to the same room
*/
async function msgerAddPeers(peers) {
// console.log("peers", peers);
// add all current Participants
for (const peer_id in peers) {
const peer_name = peers[peer_id]['peer_name'];
const peer_avatar = peers[peer_id]['peer_avatar'];
// bypass insert to myself in the list :)
if (peer_id != myPeerId && peer_name) {
const exsistMsgerPrivateDiv = getId(peer_id + '_pMsgDiv');
// if there isn't add it....
if (!exsistMsgerPrivateDiv) {
//
const chatAvatar =
peer_avatar && isImageURL(peer_avatar)
? peer_avatar
: isValidEmail(peer_name)
? genGravatar(peer_name)
: genAvatarSvg(peer_name, 24);
const msgerPrivateDiv = `
<div id="${peer_id}_pMsgDiv" class="msger-peer-inputarea">
<span style="display: none">${peer_name}</span>
<img id="${peer_id}_pMsgAvatar" class="peer-img" src="${chatAvatar}">
<textarea
rows="1"
cols="1"
id="${peer_id}_pMsgInput"
class="msger-input"
placeholder="Write private message..."
></textarea>
<button id="${peer_id}_pMsgBtn" class="${className.msgPrivate}" value="${peer_name}"></button>
</div>
`;
msgerCPList.insertAdjacentHTML('beforeend', msgerPrivateDiv);
msgerCPList.scrollTop += 500;
const msgerPrivateMsgInput = getId(peer_id + '_pMsgInput');
const msgerPrivateBtn = getId(peer_id + '_pMsgBtn');
addMsgerPrivateBtn(msgerPrivateBtn, msgerPrivateMsgInput, myPeerId);
}
}
}
}
/**
* Search peer by name in chat room lists to send the private messages
*/
function searchPeer() {
const searchPeerBarName = getId('searchPeerBarName').value.toLowerCase();
const msgerPeerInputarea = getEcN('msger-peer-inputarea');
for (let i = 0; i < msgerPeerInputarea.length; i++) {
const span = msgerPeerInputarea[i].getElementsByTagName('span')[0];
//console.log(span);
span && span.innerText.toLowerCase().includes(searchPeerBarName)
? elemDisplay(msgerPeerInputarea[i], true, 'flex')
: elemDisplay(msgerPeerInputarea[i], false);
}
}
/**
* Remove participant from chat room lists
* @param {string} peer_id socket.id
*/
function msgerRemovePeer(peer_id) {
const msgerPrivateDiv = getId(peer_id + '_pMsgDiv');
if (msgerPrivateDiv) {
let peerToRemove = msgerPrivateDiv.firstChild;
while (peerToRemove) {
msgerPrivateDiv.removeChild(peerToRemove);
peerToRemove = msgerPrivateDiv.firstChild;
}
msgerPrivateDiv.remove();
}
}
/**
* Setup msger buttons to send private messages
* @param {object} msgerPrivateBtn chat private message send button
* @param {object} msgerPrivateMsgInput chat private message text input
* @param {string} peerId chat peer_id
*/
function addMsgerPrivateBtn(msgerPrivateBtn, msgerPrivateMsgInput, peerId) {
// add button to send private messages
msgerPrivateBtn.addEventListener('click', (e) => {
e.preventDefault();
sendPrivateMessage();
});
// Number 13 is the "Enter" key on the keyboard
msgerPrivateMsgInput.addEventListener('keyup', (e) => {
if (e.keyCode === 13) {
e.preventDefault();
sendPrivateMessage();
}
});
msgerPrivateMsgInput.onpaste = () => {
isChatPasteTxt = true;
};
function sendPrivateMessage() {
msgerPrivateMsgInput.value = filterXSS(msgerPrivateMsgInput.value.trim());
const pMsg = checkMsg(msgerPrivateMsgInput.value);
if (!pMsg) {
msgerPrivateMsgInput.value = '';
isChatPasteTxt = false;
return;
}
// sanitization to prevent XSS
msgerPrivateBtn.value = filterXSS(msgerPrivateBtn.value);
myPeerName = filterXSS(myPeerName);
if (isHtml(myPeerName) && isHtml(msgerPrivateBtn.value)) {
msgerPrivateMsgInput.value = '';
isChatPasteTxt = false;
return;
}
const toPeerName = msgerPrivateBtn.value;
emitMsg(myPeerName, myPeerAvatar, toPeerName, pMsg, true, peerId);
appendMessage(myPeerName, rightChatAvatar, 'right', pMsg, true, null, toPeerName);
msgerPrivateMsgInput.value = '';
elemDisplay(msgerCP, false);
}
}
/**
* Check Message
* @param {string} txt passed text
* @returns {string} html format
*/
function checkMsg(txt) {
const text = filterXSS(txt);
if (text.trim().length == 0) return;
if (isHtml(text)) return sanitizeHtml(text);
if (isValidHttpURL(text)) {
if (isImageURL(text)) return getImage(text);
//if (isVideoTypeSupported(text)) return getIframe(text);
return getLink(text);
}
if (isChatMarkdownOn) return marked.parse(text);
if (isChatPasteTxt && getLineBreaks(text) > 1) {
isChatPasteTxt = false;
return getPre(text);
}
if (getLineBreaks(text) > 1) return getPre(text);
console.log('CheckMsg', text);
return text;
}
/**
* Sanitize Html
* @param {string} input code
* @returns Html as string
*/
function sanitizeHtml(input) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
'/': '&#x2F;',
};
return input.replace(/[&<>"'/]/g, (m) => map[m]);
}
/**
* Check if string contain html
* @param {string} str
* @returns
*/
function isHtml(str) {
let a = document.createElement('div');
a.innerHTML = str;
for (let c = a.childNodes, i = c.length; i--; ) {
if (c[i].nodeType == 1) return true;
}
return false;
}
/**
* Check if valid URL
* @param {string} str to check
* @returns boolean true/false
*/
function isValidHttpURL(input) {
try {
new URL(input);
return true;
} catch (_) {
return false;
}
}
/**
* Check if url passed is a image
* @param {string} url to check
* @returns {boolean} true/false
*/
function isImageURL(input) {
if (!input || typeof input !== 'string') return false;
try {
const url = new URL(input);
return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.svg'].some((ext) =>
url.pathname.toLowerCase().endsWith(ext)
);
} catch (e) {
return false;
}
}
/**
* Get image
* @param {string} text
* @returns img
*/
function getImage(text) {
const url = filterXSS(text);
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;
}
/**
* Get Link
* @param {string} text
* @returns a href
*/
function getLink(text) {
const url = filterXSS(text);
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;
}
/**
* Get pre
* @param {string} txt
* @returns pre
*/
function getPre(txt) {
const text = filterXSS(txt);
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;
}
/**
* Get IFrame from URL
* @param {string} text
* @returns html iframe
*/
function getIframe(text) {
const url = filterXSS(text);
const iframe = document.createElement('iframe');
const div = document.createElement('div');
const is_youtube = getVideoType(url) == 'na' ? true : false;
const video_audio_url = is_youtube ? 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;
}
/**
* Get text Line breaks
* @param {string} text
* @returns integer lines
*/
function getLineBreaks(text) {
return (text.match(/\n/g) || []).length;
}
/**
* Check chat input line breaks and value length
*/
function checkLineBreaks() {
msgerInput.style.height = '';
if (getLineBreaks(msgerInput.value) > 0 || msgerInput.value.length > 50) {
msgerInput.style.height = '200px';
}
}
/**
* Format date
* @param {object} date
* @returns {string} date format h:m:s
*/
function getFormatDate(date) {
const time = date.toTimeString().split(' ')[0];
return `${time}`;
}
/**
* Send message over Secure dataChannels
* @param {string} from peer name
* @param {string} fromAvatar peer avatar
* @param {string} to peer name
* @param {string} msg message to send
* @param {boolean} privateMsg if is a private message
* @param {string} id peer_id
*/
function emitMsg(from, fromAvatar, to, msg, privateMsg, id) {
if (!msg) return;
// sanitize all params
const getFrom = filterXSS(from);
const getFromAvatar = filterXSS(fromAvatar);
const getFromId = filterXSS(myPeerId);
const getTo = filterXSS(to);
const getMsg = filterXSS(msg);
const getPrivateMsg = filterXSS(privateMsg);
const getId = filterXSS(id);
const chatMessage = {
type: 'chat',
from: getFrom,
fromAvatar: getFromAvatar,
fromId: getFromId,
id: getId,
to: getTo,
msg: getMsg,
privateMsg: getPrivateMsg,
};
console.log('Send msg', chatMessage);
sendToDataChannel(chatMessage);
}
/**
* Read ChatGPT incoming message
* https://platform.openai.com/docs/introduction
* @param {string} msg
*/
async function getChatGPTmessage(msg) {
console.log('Send ChatGPT message:', msg);
signalingSocket
.request('data', {
room_id: roomId,
peer_id: myPeerId,
peer_name: myPeerName,
method: 'getChatGPT',
params: {
time: getDataTimeString(),
prompt: msg,
context: chatGPTcontext,
},
})
.then(
function (completion) {
if (!completion) return;
const { message, context } = completion;
chatGPTcontext = context ? context : [];
setPeerChatAvatarImgName('left', 'ChatGPT');
appendMessage('ChatGPT', leftChatAvatar, 'left', message, true);
cleanMessageInput();
speechInMessages ? speechMessage(true, 'ChatGPT', message) : playSound('message');
}.bind(this)
)
.catch((err) => {
console.log('ChatGPT error:', err);
});
}
/**
* Hide - Show emoji picker div
*/
function hideShowEmojiPicker() {
if (!isChatEmojiVisible) {
elemDisplay(msgerEmojiPicker, true, 'block');
setColor(msgerEmojiBtn, '#FFFF00');
isChatEmojiVisible = true;
return;
}
elemDisplay(msgerEmojiPicker, false);
setColor(msgerEmojiBtn, '#FFFFFF');
isChatEmojiVisible = false;
}
/**
* Download Chat messages in json format
* https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
*/
function downloadChatMsgs() {
let a = document.createElement('a');
a.href = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(chatMessages, null, 1));
a.download = getDataTimeString() + '-CHAT.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
playSound('download');
}
/**
* Download Captions in json format
* https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
*/
function downloadCaptions() {
let a = document.createElement('a');
a.href = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(transcripts, null, 1));
a.download = getDataTimeString() + roomId + '-CAPTIONS.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
playSound('download');
}
/**
* Hide - show my settings
*/
function hideShowMySettings() {
if (!isMySettingsVisible) {
playSound('newMessage');
// adapt it for mobile
if (isMobileDevice) {
mySettings.style.setProperty('width', '100%');
mySettings.style.setProperty('height', '100%');
setSP('--mySettings-select-w', '99%');
}
// my current peer name
myPeerNameSet.placeholder = myPeerName;
// center screen on show
mySettings.style.top = '50%';
mySettings.style.left = '50%';
elemDisplay(mySettings, true, 'block');
setTippy(mySettingsBtn, 'Close the settings', bottomButtonsPlacement);
isMySettingsVisible = true;
videoMediaContainer.style.opacity = 0.3;
return;
}
elemDisplay(mySettings, false);
setTippy(mySettingsBtn, 'Open the settings', bottomButtonsPlacement);
isMySettingsVisible = false;
videoMediaContainer.style.opacity = 1;
}
/**
* Handle html tab settings
* https://www.w3schools.com/howto/howto_js_tabs.asp
* @param {object} evt event
* @param {string} tabName name of the tab to open
*/
function openTab(evt, tabName) {
const tabN = getId(tabName);
const tabContent = getEcN('tabcontent');
const tabLinks = getEcN('tablinks');
let i;
for (i = 0; i < tabContent.length; i++) {
elemDisplay(tabContent[i], false);
}
for (i = 0; i < tabLinks.length; i++) {
tabLinks[i].className = tabLinks[i].className.replace(' active', '');
}
elemDisplay(tabN, true, 'block');
evt.currentTarget.className += ' active';
}
/**
* Update myPeerName to other peers in the room
*/
async function updateMyPeerName() {
// myNewPeerName empty
if (!myPeerNameSet.value) return;
// check if peer name is already in use in the room
if (await checkUserName(myPeerNameSet.value)) {
myPeerNameSet.value = '';
return userLog('warning', 'Username is already in use!');
}
// prevent xss execution itself
myPeerNameSet.value = filterXSS(myPeerNameSet.value);
// prevent XSS injection to remote peer
if (isHtml(myPeerNameSet.value)) {
myPeerNameSet.value = '';
return userLog('warning', 'Invalid name!');
}
const myNewPeerName = myPeerNameSet.value;
const myOldPeerName = myPeerName;
myPeerName = myNewPeerName;
myVideoParagraph.innerText = myPeerName + ' (me)';
sendToServer('peerName', {
room_id: roomId,
peer_name_old: myOldPeerName,
peer_name_new: myPeerName,
peer_avatar: myPeerAvatar,
});
myPeerNameSet.value = '';
myPeerNameSet.placeholder = myPeerName;
window.localStorage.peer_name = myPeerName;
setPeerAvatarImgName('myVideoAvatarImage', myPeerName, myPeerAvatar);
setPeerAvatarImgName('myProfileAvatar', myPeerName, myPeerAvatar);
setPeerChatAvatarImgName('right', myPeerName, myPeerAvatar);
userLog('toast', 'My name changed to ' + myPeerName);
}
/**
* Append updated peer name to video player
* @param {object} config data
*/
function handlePeerName(config) {
const { peer_id, peer_name, peer_avatar } = config;
const videoName = getId(peer_id + '_name');
if (videoName) videoName.innerText = peer_name;
// change also avatar and btn value - name on chat lists....
const msgerPeerName = getId(peer_id + '_pMsgBtn');
const msgerPeerAvatar = getId(peer_id + '_pMsgAvatar');
if (msgerPeerName) msgerPeerName.value = peer_name;
if (msgerPeerAvatar) {
msgerPeerAvatar.src =
peer_avatar && isImageURL(peer_avatar)
? peer_avatar
: isValidEmail(peer_name)
? genGravatar(peer_name)
: genAvatarSvg(peer_name, 32);
}
// refresh also peer video avatar name
setPeerAvatarImgName(peer_id + '_avatar', peer_name, peer_avatar);
}
/**
* Send my Video-Audio-Hand... status
* @param {string} element typo
* @param {boolean} status true/false
*/
async function emitPeerStatus(element, status) {
sendToServer('peerStatus', {
room_id: roomId,
peer_name: myPeerName,
peer_id: myPeerId,
element: element,
status: status,
});
}
/**
* Handle hide myself from room view
* @param {boolean} isHideMeActive
*/
function handleHideMe(isHideMeActive) {
if (isHideMeActive) {
if (isVideoPinned) myVideoPinBtn.click();
elemDisplay(myVideoWrap, false);
setColor(hideMeBtn, 'red');
hideMeBtn.className = className.hideMeOn;
playSound('off');
} else {
elemDisplay(myVideoWrap, true, 'inline-block');
hideMeBtn.className = className.hideMeOff;
setColor(hideMeBtn, 'var(--btn-bar-bg-color)');
playSound('on');
}
resizeVideoMedia();
}
/**
* Set my Hand Status and Icon
*/
function setMyHandStatus() {
myHandStatus = !myHandStatus;
if (myHandStatus) {
// Raise hand
setColor(myHandBtn, 'green');
elemDisplay(myHandStatusIcon, true);
setTippy(myHandBtn, 'Raise your hand', bottomButtonsPlacement);
playSound('raiseHand');
} else {
// Lower hand
setColor(myHandBtn, 'var(--btn-bar-bg-color)');
elemDisplay(myHandStatusIcon, false);
setTippy(myHandBtn, 'Lower your hand', bottomButtonsPlacement);
}
emitPeerStatus('hand', myHandStatus);
}
/**
* Set My Audio Status Icon and Title
* @param {boolean} status of my audio
*/
function setMyAudioStatus(status) {
console.log('My audio status', status);
const audioClassName = status ? className.audioOn : className.audioOff;
audioBtn.className = audioClassName;
myAudioStatusIcon.className = audioClassName;
// send my audio status to all peers in the room
emitPeerStatus('audio', status);
setTippy(myAudioStatusIcon, status ? 'My audio is on' : 'My audio is off', 'bottom');
setTippy(audioBtn, status ? 'Stop the audio' : 'Start the audio', bottomButtonsPlacement);
status ? playSound('on') : playSound('off');
}
/**
* Set My Video Status Icon and Title
* @param {boolean} status of my video
*/
function setMyVideoStatus(status) {
console.log('My video status', status);
// On video OFF display my video avatar name
if (myVideoAvatarImage) {
status ? elemDisplay(myVideoAvatarImage, false) : elemDisplay(myVideoAvatarImage, true, 'block');
}
if (myVideoStatusIcon) {
myVideoStatusIcon.className = status ? className.videoOn : className.videoOff;
}
// send my video status to all peers in the room
emitPeerStatus('video', status);
if (!isMobileDevice) {
if (myVideoStatusIcon) setTippy(myVideoStatusIcon, status ? 'My video is on' : 'My video is off', 'bottom');
setTippy(videoBtn, status ? 'Stop the video' : 'Start the video', bottomButtonsPlacement);
}
if (status) {
elemDisplay(myVideo, true, 'block');
elemDisplay(initVideo, true, 'block');
playSound('on');
} else {
elemDisplay(myVideo, false);
elemDisplay(initVideo, false);
playSound('off');
}
}
/**
* Handle peer audio - video - hand - privacy status
* @param {object} config data
*/
function handlePeerStatus(config) {
//
const { peer_id, peer_name, element, status } = config;
switch (element) {
case 'video':
setPeerVideoStatus(peer_id, status);
break;
case 'audio':
setPeerAudioStatus(peer_id, status);
break;
case 'hand':
setPeerHandStatus(peer_id, peer_name, status);
break;
case 'privacy':
setVideoPrivacyStatus(peer_id + '___video', status);
break;
default:
break;
}
}
/**
* Set Participant Hand Status Icon and Title
* @param {string} peer_id socket.id
* @param {string} peer_name peer name
* @param {boolean} status of the hand
*/
function setPeerHandStatus(peer_id, peer_name, status) {
const peerHandStatus = getId(peer_id + '_handStatus');
if (status) {
elemDisplay(peerHandStatus, true);
userLog('toast', `${icons.user} ${peer_name} \n has raised the hand!`);
playSound('raiseHand');
} else {
elemDisplay(peerHandStatus, false);
}
}
/**
* Set Participant Audio Status and toggle Audio Volume
* @param {string} peer_id socket.id
* @param {boolean} status of peer audio
*/
function setPeerAudioStatus(peer_id, status) {
const peerAudioStatus = getId(peer_id + '_audioStatus');
const peerAudioVolume = getId(peer_id + '_audioVolume');
if (peerAudioStatus) {
peerAudioStatus.className = status ? className.audioOn : className.audioOff;
setTippy(peerAudioStatus, status ? 'Participant audio is on' : 'Participant audio is off', 'bottom');
status ? playSound('on') : playSound('off');
}
if (peerAudioVolume) {
elemDisplay(peerAudioVolume, status);
}
}
/**
* Handle Peer audio volume 0/100
* @param {string} audioVolumeId audio volume input id
* @param {string} mediaId peer audio id
*/
function handleAudioVolume(audioVolumeId, mediaId) {
const media = getId(mediaId);
const audioVolume = getId(audioVolumeId);
if (audioVolume && media) {
audioVolume.style.maxWidth = '40px';
audioVolume.style.display = 'inline';
audioVolume.style.cursor = 'pointer';
audioVolume.value = 100;
audioVolume.addEventListener('input', () => {
media.volume = audioVolume.value / 100;
});
} else {
if (audioVolume) elemDisplay(audioVolume, false);
}
}
/**
* Mute Audio to specific user in the room
* @param {string} peer_id socket.id
*/
function handlePeerAudioBtn(peer_id) {
if (!buttons.remote.audioBtnClickAllowed) return;
const peerAudioBtn = getId(peer_id + '_audioStatus');
peerAudioBtn.onclick = () => {
if (peerAudioBtn.className === className.audioOn) {
isPresenter
? disablePeer(peer_id, 'audio')
: msgPopup('warning', 'Only the presenter can mute participants', 'top-end', 4000);
}
};
}
/**
* Hide Video to specified peer in the room
* @param {string} peer_id socket.id
*/
function handlePeerVideoBtn(peer_id) {
if (!useVideo || !buttons.remote.videoBtnClickAllowed) return;
const peerVideoBtn = getId(peer_id + '_videoStatus');
peerVideoBtn.onclick = () => {
if (peerVideoBtn.className === className.videoOn) {
isPresenter
? disablePeer(peer_id, 'video')
: msgPopup('warning', 'Only the presenter can hide participants', 'top-end', 4000);
}
};
}
function handlePeerGeoLocation(peer_id) {
const remoteGeoLocationBtn = getId(peer_id + '_geoLocation');
remoteGeoLocationBtn.onclick = () => {
isPresenter
? geo.askPeerGeoLocation(peer_id)
: msgPopup('warning', 'Only the presenter can ask geolocation to the participants', 'top-end', 4000);
};
}
/**
* Send Private Message to specific peer
* @param {string} peer_id socket.id
* @param {string} toPeerName peer name to send message
*/
function handlePeerPrivateMsg(peer_id, toPeerName) {
const peerPrivateMsg = getId(peer_id + '_privateMsg');
peerPrivateMsg.onclick = (e) => {
e.preventDefault();
sendPrivateMsgToPeer(myPeerId, toPeerName);
};
}
/**
* Send Private messages to peers
* @param {string} toPeerId
* @param {string} toPeerName
*/
function sendPrivateMsgToPeer(toPeerId, toPeerName) {
Swal.fire({
background: swBg,
position: 'center',
imageUrl: images.message,
title: 'Send private message',
input: 'text',
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);
const pMsg = checkMsg(result.value);
if (!pMsg) {
isChatPasteTxt = false;
return;
}
emitMsg(myPeerName, myPeerAvatar, toPeerName, pMsg, true, toPeerId);
appendMessage(myPeerName, rightChatAvatar, 'right', pMsg, true, null, toPeerName);
userLog('toast', 'Message sent to ' + toPeerName + ' 👍');
}
});
}
/**
* Handle peer send file
* @param {string} peer_id
*/
function handlePeerSendFile(peer_id) {
const peerFileSendBtn = getId(peer_id + '_shareFile');
peerFileSendBtn.onclick = () => {
selectFileToShare(peer_id);
};
}
/**
* Send video - audio URL to specific peer
* @param {string} peer_id socket.id
*/
function handlePeerVideoAudioUrl(peer_id) {
const peerYoutubeBtn = getId(peer_id + '_videoAudioUrl');
peerYoutubeBtn.onclick = () => {
sendVideoUrl(peer_id);
};
}
/**
* Set Participant Video Status Icon and Title
* @param {string} peer_id socket.id
* @param {boolean} status of peer video
*/
function setPeerVideoStatus(peer_id, status) {
const peerVideoPlayer = getId(peer_id + '___video');
const peerVideoAvatarImage = getId(peer_id + '_avatar');
const peerVideoStatus = getId(peer_id + '_videoStatus');
if (status) {
if (peerVideoPlayer) elemDisplay(peerVideoPlayer, true, 'block');
if (peerVideoAvatarImage) elemDisplay(peerVideoAvatarImage, false);
if (peerVideoStatus) {
peerVideoStatus.className = className.videoOn;
setTippy(peerVideoStatus, 'Participant video is on', 'bottom');
playSound('on');
}
} else {
if (peerVideoPlayer) elemDisplay(peerVideoPlayer, false);
if (peerVideoAvatarImage) elemDisplay(peerVideoAvatarImage, true, 'block');
if (peerVideoStatus) {
peerVideoStatus.className = className.videoOff;
setTippy(peerVideoStatus, 'Participant video is off', 'bottom');
playSound('off');
}
}
}
/**
* Emit actions to all peers in the same room except yourself
* @param {object} peerAction to all peers
*/
async function emitPeersAction(peerAction) {
if (!thereArePeerConnections()) return;
sendToServer('peerAction', {
room_id: roomId,
peer_name: myPeerName,
peer_avatar: myPeerAvatar,
peer_id: myPeerId,
peer_uuid: myPeerUUID,
peer_use_video: useVideo,
peer_action: peerAction,
send_to_all: true,
});
}
/**
* Emit actions to specified peer in the same room
* @param {string} peer_id socket.id
* @param {object} peerAction to specified peer
*/
async function emitPeerAction(peer_id, peerAction) {
if (!thereArePeerConnections()) return;
sendToServer('peerAction', {
room_id: roomId,
peer_id: peer_id,
peer_avatar: myPeerAvatar,
peer_use_video: useVideo,
peer_name: myPeerName,
peer_action: peerAction,
send_to_all: false,
});
}
/**
* Handle received peer actions
* @param {object} config data
*/
function handlePeerAction(config) {
console.log('Handle peer action: ', config);
const { peer_id, peer_name, peer_avatar, peer_use_video, peer_action } = config;
switch (peer_action) {
case 'muteAudio':
setMyAudioOff(peer_name);
break;
case 'hideVideo':
setMyVideoOff(peer_name);
break;
case 'recStart':
notifyRecording(peer_id, peer_name, peer_avatar, 'Start');
break;
case 'recStop':
notifyRecording(peer_id, peer_name, peer_avatar, 'Stop');
break;
case 'screenStart':
handleScreenStart(peer_id);
break;
case 'screenStop':
handleScreenStop(peer_id, peer_use_video);
break;
case 'ejectAll':
handleKickedOut(config);
break;
default:
break;
}
}
/**
* Handle commands from the server
* @param {object} config data
*/
function handleCmd(config) {
console.log('Handle cmd: ', config);
const { action, data } = config;
switch (action) {
case 'geoLocation':
// Peer is requesting your location
geo.confirmPeerGeoLocation(data);
break;
case 'geoLocationOK':
case 'geoLocationKO':
// Peer responded with their location or an error/denial
geo.handleGeoPeerLocation(config);
break;
//....
default:
break;
}
}
/**
* Handle incoming message
* @param {object} message
*/
function handleMessage(message) {
console.log('Got message', message);
switch (message.type) {
case 'roomEmoji':
handleEmoji(message);
break;
//....
default:
break;
}
}
/**
* Handle room emoji reaction
* @param {object} message
* @param {integer} duration time in ms
*/
function handleEmoji(message, duration = 5000) {
if (userEmoji) {
const emojiDisplay = document.createElement('div');
emojiDisplay.className = 'animate__animated animate__backInUp';
emojiDisplay.style.padding = '10px';
emojiDisplay.style.fontSize = '2vh';
emojiDisplay.style.color = '#FFF';
emojiDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
emojiDisplay.style.borderRadius = '10px';
emojiDisplay.style.marginBottom = '5px';
emojiDisplay.innerText = `${message.emoji} ${message.peer_name}`;
userEmoji.appendChild(emojiDisplay);
setTimeout(() => {
emojiDisplay.remove();
}, duration);
handleEmojiSound(message);
}
}
/**
* Play emoji sound
* https://freesound.org/
* https://cloudconvert.com
* @param {object} message
*/
function handleEmojiSound(message) {
const path = '../sounds/emoji/';
switch (message.shortcodes) {
case ':+1:':
case ':ok_hand:':
playSound('ok', true, path);
break;
case ':-1:':
playSound('boo', true, path);
break;
case ':clap:':
playSound('applause', true, path);
break;
case ':smiley:':
case ':grinning:':
playSound('smile', true, path);
break;
case ':joy:':
playSound('laughs', true, path);
break;
case ':tada:':
playSound('congrats', true, path);
break;
case ':open_mouth:':
playSound('woah', true, path);
break;
case ':trumpet:':
playSound('trombone', true, path);
break;
case ':kissing_heart:':
playSound('kiss', true, path);
break;
case ':heart:':
case ':hearts:':
playSound('heart', true, path);
break;
case ':rocket:':
playSound('rocket', true, path);
break;
case ':sparkles:':
case ':star:':
case ':star2:':
case ':dizzy:':
playSound('tinkerbell', true, path);
break;
// ...
default:
break;
}
}
/**
* Handle Screen Start
* @param {string} peer_id
*/
function handleScreenStart(peer_id) {
const remoteVideoAvatarImage = getId(peer_id + '_avatar');
const remoteVideoStatusBtn = getId(peer_id + '_videoStatus');
const remoteVideoStream = getId(peer_id + '___video');
if (remoteVideoStatusBtn) {
remoteVideoStatusBtn.className = className.videoOn;
setTippy(remoteVideoStatusBtn, 'Participant screen share is on', 'bottom');
}
if (remoteVideoStream) {
getId(peer_id + '_pinUnpin').click();
remoteVideoStream.style.objectFit = 'contain';
remoteVideoStream.style.name = peer_id + '_typeScreen';
}
if (remoteVideoAvatarImage) elemDisplay(remoteVideoAvatarImage, false);
}
/**
* Handle Screen Stop
* @param {string} peer_id
* @param {boolean} peer_use_video
*/
function handleScreenStop(peer_id, peer_use_video) {
const remoteVideoStream = getId(peer_id + '___video');
const remoteVideoAvatarImage = getId(peer_id + '_avatar');
const remoteVideoStatusBtn = getId(peer_id + '_videoStatus');
if (remoteVideoStatusBtn) {
remoteVideoStatusBtn.className = className.videoOff;
setTippy(remoteVideoStatusBtn, 'Participant screen share is off', 'bottom');
}
if (remoteVideoStream) {
if (isVideoPinned) getId(peer_id + '_pinUnpin').click();
remoteVideoStream.style.objectFit = 'var(--video-object-fit)';
remoteVideoStream.style.name = peer_id + '_typeCam';
adaptAspectRatio();
}
if (remoteVideoAvatarImage && remoteVideoStream && !peer_use_video) {
elemDisplay(remoteVideoAvatarImage, true, 'block');
remoteVideoStream.srcObject.getVideoTracks().forEach((track) => {
track.stop();
// track.enabled = false;
});
elemDisplay(remoteVideoStream, false);
} else {
if (remoteVideoAvatarImage) elemDisplay(remoteVideoAvatarImage, false);
}
}
/**
* Set my Audio off and Popup the peer name that performed this action
* @param {string} peer_name peer name
*/
function setMyAudioOff(peer_name) {
if (myAudioStatus === false || !useAudio) return;
localAudioMediaStream.getAudioTracks()[0].enabled = false;
myAudioStatus = localAudioMediaStream.getAudioTracks()[0].enabled;
audioBtn.className = className.audioOff;
setMyAudioStatus(myAudioStatus);
userLog('toast', `${icons.user} ${peer_name} \n has disabled your audio`);
playSound('off');
}
/**
* Set my Audio on and Popup the peer name that performed this action
* @param {string} peer_name peer name
*/
function setMyAudioOn(peer_name) {
if (myAudioStatus === true || !useAudio) return;
localAudioMediaStream.getAudioTracks()[0].enabled = true;
myAudioStatus = localAudioMediaStream.getAudioTracks()[0].enabled;
audioBtn.className = className.audioOn;
setMyAudioStatus(myAudioStatus);
userLog('toast', `${icons.user} ${peer_name} \n has enabled your audio`);
playSound('on');
}
/**
* Set my Video off and Popup the peer name that performed this action
* @param {string} peer_name peer name
*/
function setMyVideoOff(peer_name) {
if (!useVideo) return;
//if (myVideoStatus === false || !useVideo) return;
localVideoMediaStream.getVideoTracks()[0].enabled = false;
myVideoStatus = localVideoMediaStream.getVideoTracks()[0].enabled;
videoBtn.className = className.videoOff;
setMyVideoStatus(myVideoStatus);
userLog('toast', `${icons.user} ${peer_name} \n has disabled your video`);
playSound('off');
}
/**
* Mute or Hide everyone except yourself
* @param {string} element type audio/video
*/
function disableAllPeers(element) {
if (!thereArePeerConnections()) {
return userLog('info', 'No participants detected');
}
Swal.fire({
background: swBg,
position: 'center',
imageUrl: element == 'audio' ? images.audioOff : images.videoOff,
title: element == 'audio' ? 'Mute everyone except yourself?' : 'Hide everyone except yourself?',
text:
element == 'audio'
? "Once muted, you won't be able to unmute them, but they can unmute themselves at any time."
: "Once hided, you won't be able to unhide them, but they can unhide themselves at any time.",
showDenyButton: true,
confirmButtonText: element == 'audio' ? `Mute` : `Hide`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
switch (element) {
case 'audio':
userLog('toast', 'Mute everyone 👍');
emitPeersAction('muteAudio');
break;
case 'video':
userLog('toast', 'Hide everyone 👍');
emitPeersAction('hideVideo');
break;
default:
break;
}
}
});
}
/**
* Eject all participants in the room expect yourself
*/
function ejectEveryone() {
if (!thereArePeerConnections()) {
return userLog('info', 'No participants detected');
}
Swal.fire({
background: swBg,
imageUrl: images.leave,
position: 'center',
title: 'Eject everyone except yourself?',
text: 'Are you sure to want eject all participants from the room?',
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
emitPeersAction('ejectAll');
}
});
}
/**
* Mute or Hide specific peer
* @param {string} peer_id socket.id
* @param {string} element type audio/video
*/
function disablePeer(peer_id, element) {
if (!thereArePeerConnections()) {
return userLog('info', 'No participants detected');
}
Swal.fire({
background: swBg,
position: 'center',
imageUrl: element == 'audio' ? images.audioOff : images.videoOff,
title: element == 'audio' ? 'Mute this participant?' : 'Hide this participant?',
text:
element == 'audio'
? "Once muted, you won't be able to unmute them, but they can unmute themselves at any time."
: "Once hided, you won't be able to unhide them, but they can unhide themselves at any time.",
showDenyButton: true,
confirmButtonText: element == 'audio' ? `Mute` : `Hide`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
switch (element) {
case 'audio':
userLog('toast', 'Mute audio 👍');
emitPeerAction(peer_id, 'muteAudio');
break;
case 'video':
userLog('toast', 'Hide video 👍');
emitPeerAction(peer_id, 'hideVideo');
break;
default:
break;
}
}
});
}
/**
* Handle Room action
* @param {object} config data
* @param {boolean} emit data to signaling server
*/
function handleRoomAction(config, emit = false) {
const { action } = config;
if (emit) {
const thisConfig = {
room_id: roomId,
peer_id: myPeerId,
peer_name: myPeerName,
peer_uuid: myPeerUUID,
action: action,
password: null,
};
switch (action) {
case 'lock':
playSound('newMessage');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: true,
background: swBg,
imageUrl: images.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';
thisRoomPassword = pwd;
},
}).then((result) => {
if (result.isConfirmed) {
thisConfig.password = thisRoomPassword;
sendToServer('roomAction', thisConfig);
handleRoomStatus(thisConfig);
}
});
break;
case 'unlock':
sendToServer('roomAction', thisConfig);
handleRoomStatus(thisConfig);
break;
default:
break;
}
} else {
// data coming from signaling server
handleRoomStatus(config);
}
}
/**
* Handle room status
* @param {object} config data
*/
function handleRoomStatus(config) {
const { action, peer_name, password } = config;
switch (action) {
case 'lock':
playSound('locked');
userLog('toast', `${icons.user} ${peer_name} \n has 🔒 LOCKED the room by password`, 'top-end');
elemDisplay(lockRoomBtn, false);
elemDisplay(unlockRoomBtn, true);
isRoomLocked = true;
break;
case 'unlock':
userLog('toast', `${icons.user} ${peer_name} \n has 🔓 UNLOCKED the room`, 'top-end');
elemDisplay(unlockRoomBtn, false);
elemDisplay(lockRoomBtn, true);
isRoomLocked = false;
break;
case 'checkPassword':
isRoomLocked = true;
password == 'OK' ? joinToChannel() : handleRoomLocked();
break;
default:
break;
}
}
/**
* Room is locked you provide a wrong password, can't access!
*/
function handleRoomLocked() {
playSound('eject');
console.log('Room is Locked, try with another one');
Swal.fire({
allowOutsideClick: false,
background: swBg,
position: 'center',
imageUrl: images.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) openURL('/newcall');
});
}
/**
* Try to unlock the room by providing a valid password
*/
function handleUnlockTheRoom() {
playSound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
imageUrl: images.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';
thisRoomPassword = pwd;
},
}).then(() => {
const config = {
room_id: roomId,
peer_name: myPeerName,
action: 'checkPassword',
password: thisRoomPassword,
};
sendToServer('roomAction', config);
elemDisplay(lockRoomBtn, false);
elemDisplay(unlockRoomBtn, true);
});
}
/**
* Handle whiteboard toogle
*/
function handleWhiteboardToggle() {
thereArePeerConnections() ? whiteboardAction(getWhiteboardAction('toggle')) : toggleWhiteboard();
}
/**
* Toggle Lock/Unlock whiteboard
*/
function toggleLockUnlockWhiteboard() {
wbIsLock = !wbIsLock;
const btnToShow = wbIsLock ? whiteboardUnlockBtn : whiteboardLockBtn;
const btnToHide = wbIsLock ? whiteboardLockBtn : whiteboardUnlockBtn;
const btnColor = wbIsLock ? 'red' : 'white';
const action = wbIsLock ? 'lock' : 'unlock';
elemDisplay(btnToShow, true, 'flex');
elemDisplay(btnToHide, false);
setColor(whiteboardUnlockBtn, btnColor);
whiteboardAction(getWhiteboardAction(action));
if (wbIsLock) {
userLog('toast', 'The whiteboard is locked. \n The participants cannot interact with it.');
playSound('locked');
}
}
/**
* Whiteboard: Show-Hide
*/
function toggleWhiteboard() {
if (!wbIsOpen) {
playSound('newMessage');
setTippy(whiteboardBtn, 'Close the Whiteboard', placement);
} else {
setTippy(whiteboardBtn, 'Open the Whiteboard', placement);
}
whiteboard.classList.toggle('show');
whiteboard.style.top = '50%';
whiteboard.style.left = '50%';
wbIsOpen = !wbIsOpen;
}
/**
* Whiteboard: setup
*/
function setupWhiteboard() {
setupWhiteboardCanvas();
setupWhiteboardCanvasSize();
setupWhiteboardLocalListners();
}
/**
* Whiteboard: setup canvas
*/
function setupWhiteboardCanvas() {
wbCanvas = new fabric.Canvas('wbCanvas');
wbCanvas.freeDrawingBrush.color = '#FFFFFF';
wbCanvas.freeDrawingBrush.width = 3;
whiteboardIsDrawingMode(true);
}
/**
* Whiteboard: setup canvas size
*/
function setupWhiteboardCanvasSize() {
const optimalSize = [wbWidth, wbHeight];
const scaleFactorX = window.innerWidth / optimalSize[0];
const scaleFactorY = window.innerHeight / optimalSize[1];
if (scaleFactorX < scaleFactorY && scaleFactorX < 1) {
wbCanvas.setWidth(optimalSize[0] * scaleFactorX);
wbCanvas.setHeight(optimalSize[1] * scaleFactorX);
wbCanvas.setZoom(scaleFactorX);
setWhiteboardSize(optimalSize[0] * scaleFactorX, optimalSize[1] * scaleFactorX);
} else if (scaleFactorX > scaleFactorY && scaleFactorY < 1) {
wbCanvas.setWidth(optimalSize[0] * scaleFactorY);
wbCanvas.setHeight(optimalSize[1] * scaleFactorY);
wbCanvas.setZoom(scaleFactorY);
setWhiteboardSize(optimalSize[0] * scaleFactorY, optimalSize[1] * scaleFactorY);
} else {
wbCanvas.setWidth(optimalSize[0]);
wbCanvas.setHeight(optimalSize[1]);
wbCanvas.setZoom(1);
setWhiteboardSize(optimalSize[0], optimalSize[1]);
}
wbCanvas.calcOffset();
wbCanvas.renderAll();
}
/**
* Whiteboard: setup size
* @param {string} w width
* @param {string} h height
*/
function setWhiteboardSize(w, h) {
setSP('--wb-width', w);
setSP('--wb-height', h);
}
/**
* Set whiteboard background color
* @param {string} color whiteboard bg
*/
function setWhiteboardBgColor(color) {
const config = {
room_id: roomId,
peer_name: myPeerName,
action: 'bgcolor',
color: color,
};
whiteboardAction(config);
}
/**
* Whiteboard: drawing mode
* @param {boolean} status of drawing mode
*/
function whiteboardIsDrawingMode(status) {
wbCanvas.isDrawingMode = status;
if (status) {
setColor(whiteboardPencilBtn, 'green');
setColor(whiteboardObjectBtn, 'white');
setColor(whiteboardEraserBtn, 'white');
wbIsEraser = false;
} else {
setColor(whiteboardPencilBtn, 'white');
setColor(whiteboardObjectBtn, 'green');
}
}
/**
* Whiteboard: eraser
* @param {boolean} status if eraser on
*/
function whiteboardIsEraser(status) {
whiteboardIsDrawingMode(false);
wbIsEraser = status;
setColor(whiteboardEraserBtn, wbIsEraser ? 'green' : 'white');
}
/**
* Set color to specific element
* @param {object} elem element
* @param {string} color to set
*/
function setColor(elem, color) {
elem.style.color = color;
}
/**
* Whiteboard: Add object to canvas
* @param {string} type of object to add
*/
function whiteboardAddObj(type) {
switch (type) {
case 'imgUrl':
Swal.fire({
background: swBg,
title: 'Image URL',
input: 'text',
showCancelButton: true,
confirmButtonText: 'OK',
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
let wbCanvasImgURL = result.value;
if (isImageURL(wbCanvasImgURL)) {
fabric.Image.fromURL(wbCanvasImgURL, function (myImg) {
addWbCanvasObj(myImg);
});
} else {
userLog('error', 'The URL is not a valid image');
}
}
});
break;
case 'imgFile':
setupFileSelection('Select the image', wbImageInput, renderImageToCanvas);
break;
case 'pdfFile':
setupFileSelection('Select the PDF', wbPdfInput, renderPdfToCanvas);
break;
case 'text':
const text = new fabric.IText('Lorem Ipsum', {
top: 0,
left: 0,
fontFamily: 'Montserrat',
fill: wbCanvas.freeDrawingBrush.color,
strokeWidth: wbCanvas.freeDrawingBrush.width,
stroke: wbCanvas.freeDrawingBrush.color,
});
addWbCanvasObj(text);
break;
case 'line':
const line = new fabric.Line([50, 100, 200, 200], {
top: 0,
left: 0,
fill: wbCanvas.freeDrawingBrush.color,
strokeWidth: wbCanvas.freeDrawingBrush.width,
stroke: wbCanvas.freeDrawingBrush.color,
});
addWbCanvasObj(line);
break;
case 'circle':
const circle = new fabric.Circle({
radius: 50,
fill: 'transparent',
stroke: wbCanvas.freeDrawingBrush.color,
strokeWidth: wbCanvas.freeDrawingBrush.width,
});
addWbCanvasObj(circle);
break;
case 'rect':
const rect = new fabric.Rect({
top: 0,
left: 0,
width: 150,
height: 100,
fill: 'transparent',
stroke: wbCanvas.freeDrawingBrush.color,
strokeWidth: wbCanvas.freeDrawingBrush.width,
});
addWbCanvasObj(rect);
break;
case 'triangle':
const triangle = new fabric.Triangle({
top: 0,
left: 0,
width: 150,
height: 100,
fill: 'transparent',
stroke: wbCanvas.freeDrawingBrush.color,
strokeWidth: wbCanvas.freeDrawingBrush.width,
});
addWbCanvasObj(triangle);
break;
default:
break;
}
}
/**
* Setup Canvas file selections
* @param {string} title
* @param {string} accept
* @param {object} renderToCanvas
*/
function setupFileSelection(title, accept, renderToCanvas) {
Swal.fire({
allowOutsideClick: false,
background: swBg,
position: 'center',
title: title,
input: 'file',
html: `
<div id="dropArea">
<p>Drag and drop your file here</p>
</div>
`,
inputAttributes: {
accept: accept,
'aria-label': title,
},
didOpen: () => {
const dropArea = document.getElementById('dropArea');
dropArea.addEventListener('dragenter', handleDragEnter);
dropArea.addEventListener('dragover', handleDragOver);
dropArea.addEventListener('dragleave', handleDragLeave);
dropArea.addEventListener('drop', handleDrop);
},
showDenyButton: true,
confirmButtonText: `OK`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
renderToCanvas(result.value);
}
});
function handleDragEnter(e) {
e.preventDefault();
e.stopPropagation();
e.target.style.background = 'var(--body-bg)';
}
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
e.target.style.background = '';
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
e.target.style.background = '';
}
function handleFiles(files) {
if (files.length > 0) {
const file = files[0];
console.log('Selected file:', file);
Swal.close();
renderToCanvas(file);
}
}
}
/**
* Render Image file to Canvas
* @param {object} wbCanvasImg
*/
function renderImageToCanvas(wbCanvasImg) {
if (wbCanvasImg && wbCanvasImg.size > 0) {
let reader = new FileReader();
reader.onload = function (event) {
let imgObj = new Image();
imgObj.src = event.target.result;
imgObj.onload = function () {
let image = new fabric.Image(imgObj);
image.set({ top: 0, left: 0 }).scale(0.3);
addWbCanvasObj(image);
};
};
reader.readAsDataURL(wbCanvasImg);
}
}
/**
* Render PDF file to Canvas
* @param {object} wbCanvasPdf
*/
async function renderPdfToCanvas(wbCanvasPdf) {
if (wbCanvasPdf && wbCanvasPdf.size > 0) {
let reader = new FileReader();
reader.onload = async function (event) {
wbCanvas.requestRenderAll();
await pdfToImage(event.target.result, wbCanvas);
whiteboardIsDrawingMode(false);
wbCanvasToJson();
};
reader.readAsDataURL(wbCanvasPdf);
}
}
/**
* Promisify the FileReader
* @param {object} blob
* @returns object Data URL
*/
function readBlob(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', reject);
reader.readAsDataURL(blob);
});
}
/**
* Load PDF and return an array of canvases
* @param {object} pdfData
* @param {object} pages
* @returns canvas object
*/
async function loadPDF(pdfData, pages) {
const pdfjsLib = window['pdfjs-dist/build/pdf'];
pdfData = pdfData instanceof Blob ? await readBlob(pdfData) : pdfData;
const data = atob(pdfData.startsWith(Base64Prefix) ? pdfData.substring(Base64Prefix.length) : pdfData);
try {
const pdf = await pdfjsLib.getDocument({ data }).promise;
const numPages = pdf.numPages;
const canvases = await Promise.all(
Array.from({ length: numPages }, (_, i) => {
const pageNumber = i + 1;
if (pages && pages.indexOf(pageNumber) === -1) return null;
return pdf.getPage(pageNumber).then(async (page) => {
const viewport = page.getViewport({ scale: window.devicePixelRatio });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
};
await page.render(renderContext).promise;
return canvas;
});
})
);
return canvases.filter((canvas) => canvas !== null);
} catch (error) {
console.error('Error loading PDF:', error);
throw error;
}
}
/**
* Convert PDF to fabric.js images and add to canvas
* @param {object} pdfData
* @param {object} canvas
*/
async function pdfToImage(pdfData, canvas) {
const scale = 1 / window.devicePixelRatio;
try {
const canvases = await loadPDF(pdfData);
canvases.forEach(async (c) => {
canvas.add(
new fabric.Image(await c, {
scaleX: scale,
scaleY: scale,
})
);
});
} catch (error) {
console.error('Error converting PDF to images:', error);
throw error;
}
}
/**
* Whiteboard: add object
* @param {object} obj to add
*/
function addWbCanvasObj(obj) {
if (obj) {
wbCanvas.add(obj).setActiveObject(obj);
whiteboardIsDrawingMode(false);
wbCanvasToJson();
}
}
/**
* Whiteboard: Local listners
*/
function setupWhiteboardLocalListners() {
wbCanvas.on('mouse:down', function (e) {
mouseDown(e);
});
wbCanvas.on('mouse:up', function () {
mouseUp();
});
wbCanvas.on('mouse:move', function () {
mouseMove();
});
wbCanvas.on('object:added', function () {
objectAdded();
});
}
/**
* Whiteboard: mouse down
* @param {object} e event
* @returns
*/
function mouseDown(e) {
wbIsDrawing = true;
if (wbIsEraser && e.target) {
wbCanvas.remove(e.target);
return;
}
}
/**
* Whiteboard: mouse up
*/
function mouseUp() {
wbIsDrawing = false;
wbCanvasToJson();
}
/**
* Whiteboard: mouse move
* @returns
*/
function mouseMove() {
if (wbIsEraser) {
wbCanvas.hoverCursor = 'not-allowed';
return;
} else {
wbCanvas.hoverCursor = 'move';
}
if (!wbIsDrawing) return;
}
/**
* Whiteboard: tmp objects
*/
function objectAdded() {
if (!wbIsRedoing) wbPop = [];
wbIsRedoing = false;
}
/**
* Whiteboard: set background color
* @param {string} color to set
*/
function wbCanvasBackgroundColor(color) {
setSP('--wb-bg', color);
wbBackgroundColorEl.value = color;
wbCanvas.setBackgroundColor(color);
wbCanvas.renderAll();
}
/**
* Whiteboard: undo
*/
function wbCanvasUndo() {
if (wbCanvas._objects.length > 0) {
wbPop.push(wbCanvas._objects.pop());
wbCanvas.renderAll();
}
}
/**
* Whiteboard: redo
*/
function wbCanvasRedo() {
if (wbPop.length > 0) {
wbIsRedoing = true;
wbCanvas.add(wbPop.pop());
}
}
/**
* Whiteboard: save as images png
*/
function wbCanvasSaveImg() {
const dataURL = wbCanvas.toDataURL({
width: wbCanvas.getWidth(),
height: wbCanvas.getHeight(),
left: 0,
top: 0,
format: 'png',
});
const dataNow = getDataTimeString();
const fileName = `whiteboard-${dataNow}.png`;
saveDataToFile(dataURL, fileName);
playSound('ok');
}
/**
* Whiteboard: save data to file
* @param {object} dataURL to download
* @param {string} fileName to save
*/
function saveDataToFile(dataURL, fileName) {
const a = document.createElement('a');
elemDisplay(a, false);
a.href = dataURL;
a.download = fileName;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(dataURL);
}, 100);
}
/**
* Whiteboard: canvas objects to json
*/
function wbCanvasToJson() {
if (!isPresenter && wbIsLock) return;
if (thereArePeerConnections()) {
const config = {
room_id: roomId,
wbCanvasJson: JSON.stringify(wbCanvas.toJSON()),
};
sendToServer('wbCanvasToJson', config);
}
}
/**
* If whiteboard opened, update canvas to all peers connected
*/
async function wbUpdate() {
if (wbIsOpen && thereArePeerConnections()) {
wbCanvasToJson();
whiteboardAction(getWhiteboardAction(wbIsLock ? 'lock' : 'unlock'));
}
}
/**
* Whiteboard: json to canvas objects
* @param {object} config data
*/
function handleJsonToWbCanvas(config) {
if (!wbIsOpen) toggleWhiteboard();
wbCanvas.loadFromJSON(config.wbCanvasJson);
wbCanvas.renderAll();
if (!isPresenter && !wbCanvas.isDrawingMode && wbIsLock) {
wbDrawing(false);
}
}
/**
* Whiteboard: actions
* @param {string} action whiteboard action
* @returns {object} data
*/
function getWhiteboardAction(action) {
return {
room_id: roomId,
peer_name: myPeerName,
action: action,
};
}
/**
* Whiteboard: Clean content
*/
function confirmCleanBoard() {
playSound('newMessage');
Swal.fire({
background: swBg,
imageUrl: images.delete,
position: 'center',
title: 'Clean the board',
text: 'Are you sure you want to clean the board?',
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
whiteboardAction(getWhiteboardAction('clear'));
playSound('delete');
}
});
}
/**
* Whiteboard: actions
* @param {object} config data
*/
function whiteboardAction(config) {
if (thereArePeerConnections()) {
sendToServer('whiteboardAction', config);
}
handleWhiteboardAction(config, false);
}
/**
* Whiteboard: handle actions
* @param {object} config data
* @param {boolean} logMe popup action
*/
function handleWhiteboardAction(config, logMe = true) {
const { peer_name, action, color } = config;
if (logMe) {
userLog('toast', `${icons.user} ${peer_name} \n whiteboard action: ${action}`);
}
switch (action) {
case 'bgcolor':
wbCanvasBackgroundColor(color);
break;
case 'undo':
wbCanvasUndo();
break;
case 'redo':
wbCanvasRedo();
break;
case 'clear':
wbCanvas.clear();
break;
case 'toggle':
toggleWhiteboard();
break;
case 'lock':
if (!isPresenter) {
elemDisplay(whiteboardTitle, false);
elemDisplay(whiteboardOptions, false);
elemDisplay(whiteboardBtn, false);
wbDrawing(false);
wbIsLock = true;
}
break;
case 'unlock':
if (!isPresenter) {
elemDisplay(whiteboardTitle, true, 'flex');
elemDisplay(whiteboardOptions, true, 'inline');
elemDisplay(whiteboardBtn, true);
wbDrawing(true);
wbIsLock = false;
}
break;
//...
default:
break;
}
}
/**
* Toggle whiteboard drawing mode
* @param {boolean} status
*/
function wbDrawing(status) {
wbCanvas.isDrawingMode = status; // Disable free drawing
wbCanvas.selection = status; // Disable object selection
wbCanvas.forEachObject(function (obj) {
obj.selectable = status; // Make all objects unselectable
});
}
/**
* Create File Sharing Data Channel
* @param {string} peer_id socket.id
*/
function createFileSharingDataChannel(peer_id) {
fileDataChannels[peer_id] = peerConnections[peer_id].createDataChannel('mirotalk_file_sharing_channel');
fileDataChannels[peer_id].binaryType = 'arraybuffer';
fileDataChannels[peer_id].onopen = (event) => {
console.log('fileDataChannels created', event);
};
}
/**
* Handle File Sharing
* @param {object} data received
*/
function handleDataChannelFileSharing(data) {
if (!receiveInProgress) return;
receiveBuffer.push(data);
receivedSize += data.byteLength;
receiveProgress.value = receivedSize;
receiveFilePercentage.innerText =
'Receive progress: ' + ((receivedSize / incomingFileInfo.file.fileSize) * 100).toFixed(2) + '%';
if (receivedSize === incomingFileInfo.file.fileSize) {
elemDisplay(receiveFileDiv, false);
incomingFileData = receiveBuffer;
receiveBuffer = [];
endDownload();
}
}
/**
* Send File Data trought datachannel
* https://webrtc.github.io/samples/src/content/datachannel/filetransfer/
* https://github.com/webrtc/samples/blob/gh-pages/src/content/datachannel/filetransfer/js/main.js
*
* @param {string} peer_id peer id
* @param {boolean} broadcast sent to all or not
*/
function sendFileData(peer_id, broadcast) {
console.log('Send file ' + fileToSend.name + ' size ' + bytesToSize(fileToSend.size) + ' type ' + fileToSend.type);
sendInProgress = true;
sendFileInfo.innerText =
'File name: ' +
fileToSend.name +
'\n' +
'File type: ' +
fileToSend.type +
'\n' +
'File size: ' +
bytesToSize(fileToSend.size) +
'\n';
elemDisplay(sendFileDiv, true);
sendProgress.max = fileToSend.size;
fileReader = new FileReader();
let offset = 0;
fileReader.addEventListener('error', (err) => console.error('fileReader error', err));
fileReader.addEventListener('abort', (e) => console.log('fileReader aborted', e));
fileReader.addEventListener('load', (e) => {
if (!sendInProgress) return;
// peer to peer over DataChannels
const data = {
peer_id: peer_id,
broadcast: broadcast,
fileData: e.target.result,
};
sendFSData(data);
offset += data.fileData.byteLength;
sendProgress.value = offset;
sendFilePercentage.innerText = 'Send progress: ' + ((offset / fileToSend.size) * 100).toFixed(2) + '%';
// send file completed
if (offset === fileToSend.size) {
sendInProgress = false;
elemDisplay(sendFileDiv, false);
userLog('success', 'The file ' + fileToSend.name + ' was sent successfully.');
}
if (offset < fileToSend.size) readSlice(offset);
});
const readSlice = (o) => {
for (const peer_id in fileDataChannels) {
// https://stackoverflow.com/questions/71285807/i-am-trying-to-share-a-file-over-webrtc-but-after-some-time-it-stops-and-log-rt
if (fileDataChannels[peer_id].bufferedAmount > fileDataChannels[peer_id].bufferedAmountLowThreshold) {
fileDataChannels[peer_id].onbufferedamountlow = () => {
fileDataChannels[peer_id].onbufferedamountlow = null;
readSlice(0);
};
return;
}
}
const slice = fileToSend.slice(offset, o + chunkSize);
fileReader.readAsArrayBuffer(slice);
};
readSlice(0);
}
/**
* Send File through RTC Data Channels
* @param {object} data to sent
*/
function sendFSData(data) {
const broadcast = data.broadcast;
const peer_id_to_send = data.peer_id;
if (broadcast) {
// send to all peers
for (const peer_id in fileDataChannels) {
if (fileDataChannels[peer_id].readyState === 'open') fileDataChannels[peer_id].send(data.fileData);
}
} else {
// send to peer
for (const peer_id in fileDataChannels) {
if (peer_id_to_send == peer_id && fileDataChannels[peer_id].readyState === 'open') {
fileDataChannels[peer_id].send(data.fileData);
}
}
}
}
/**
* Abort the file transfer
*/
function abortFileTransfer() {
if (fileReader && fileReader.readyState === 1) {
fileReader.abort();
elemDisplay(sendFileDiv, false);
sendInProgress = false;
sendToServer('fileAbort', {
room_id: roomId,
peer_name: myPeerName,
});
}
}
/**
* Abort file transfer
*/
function abortReceiveFileTransfer() {
sendToServer('fileReceiveAbort', {
room_id: roomId,
peer_name: myPeerName,
});
}
/**
* Handle abort file transfer
* @param object config - peer info that abort the file transfer
*/
function handleAbortFileTransfer(config) {
console.log(`File transfer aborted by ${config.peer_name}`);
userLog('toast', `⚠️ File transfer aborted by ${config.peer_name}`);
abortFileTransfer();
}
/**
* File Transfer aborted by peer
*/
function handleFileAbort() {
receiveBuffer = [];
incomingFileData = [];
receivedSize = 0;
receiveInProgress = false;
elemDisplay(receiveFileDiv, false);
console.log('File transfer aborted');
userLog('toast', '⚠️ File transfer aborted');
}
/**
* Hide incoming file transfer
*/
function hideFileTransfer() {
elemDisplay(receiveFileDiv, false);
}
/**
* Select or Drag and Drop the File to Share
* @param {string} peer_id
* @param {boolean} broadcast send to all (default false)
*/
function selectFileToShare(peer_id, broadcast = false) {
playSound('newMessage');
Swal.fire({
allowOutsideClick: false,
background: swBg,
imageAlt: 'mirotalk-file-sharing',
imageUrl: images.share,
position: 'center',
title: 'Share file',
input: 'file',
html: `
<div id="dropArea">
<p>Drag and drop your file here</p>
</div>
`,
inputAttributes: {
accept: fileSharingInput,
'aria-label': 'Select file',
},
didOpen: () => {
const dropArea = getId('dropArea');
dropArea.addEventListener('dragenter', handleDragEnter);
dropArea.addEventListener('dragover', handleDragOver);
dropArea.addEventListener('dragleave', handleDragLeave);
dropArea.addEventListener('drop', handleDrop);
},
showDenyButton: true,
confirmButtonText: `Send`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
sendFileInformations(result.value, peer_id, broadcast);
}
});
function handleDragEnter(e) {
e.preventDefault();
e.stopPropagation();
e.target.style.background = 'var(--body-bg)';
}
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
e.target.style.background = '';
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
e.target.style.background = '';
}
function handleFiles(files) {
if (files.length > 0) {
const file = files[0];
console.log('Selected file:', file);
Swal.close();
sendFileInformations(file, peer_id, broadcast);
}
}
}
/**
* Send file informations
* @param {object} file data
* @param {string} peer_id
* @param {boolean} broadcast send to all (default false)
* @returns
*/
function sendFileInformations(file, peer_id, broadcast = false) {
fileToSend = file;
// check if valid
if (fileToSend && fileToSend.size > 0) {
// no peers in the room
if (!thereArePeerConnections()) {
return userLog('info', 'No participants detected');
}
// prevent XSS injection to remote peer (fileToSend.name is read only)
if (isHtml(fileToSend.name) || !isValidFileName(fileToSend.name))
return userLog('warning', 'Invalid file name!');
const fileInfo = {
room_id: roomId,
broadcast: broadcast,
peer_name: myPeerName,
peer_avatar: myPeerAvatar,
peer_id: peer_id,
file: {
fileName: fileToSend.name,
fileSize: fileToSend.size,
fileType: fileToSend.type,
},
};
// keep trace of sent file in chat
appendMessage(
myPeerName,
rightChatAvatar,
'right',
`${icons.fileSend} File send:
<br/>
<ul>
<li>Name: ${fileToSend.name}</li>
<li>Size: ${bytesToSize(fileToSend.size)}</li>
</ul>`,
false
);
// send some metadata about our file to peers in the room
sendToServer('fileInfo', fileInfo);
// send the File
setTimeout(() => {
sendFileData(peer_id, broadcast);
}, 1000);
} else {
userLog('error', 'File dragged not valid or empty.');
}
}
/**
* Html Json pretty print
* @param {object} obj
* @returns html pre json
*/
function toHtmlJson(obj) {
return '<pre>' + JSON.stringify(obj, null, 4) + '</pre>';
}
/**
* Get remote file info
* @param {object} config data
*/
function handleFileInfo(config) {
incomingFileInfo = config;
incomingFileData = [];
receiveBuffer = [];
receivedSize = 0;
let fileToReceiveInfo =
'From: ' +
incomingFileInfo.peer_name +
'\n' +
'Incoming file: ' +
incomingFileInfo.file.fileName +
'\n' +
'File size: ' +
bytesToSize(incomingFileInfo.file.fileSize) +
'\n' +
'File type: ' +
incomingFileInfo.file.fileType;
console.log(fileToReceiveInfo);
// generate chat avatar by peer_name
setPeerChatAvatarImgName('left', incomingFileInfo.peer_name, incomingFileInfo.peer_avatar);
// keep track of received file on chat
appendMessage(
incomingFileInfo.peer_name,
leftChatAvatar,
'left',
`${icons.fileReceive} File receive:
<br/>
<ul>
<li>From: ${incomingFileInfo.peer_name}</li>
<li>Name: ${incomingFileInfo.file.fileName}</li>
<li>Size: ${bytesToSize(incomingFileInfo.file.fileSize)}</li>
</ul>`,
!incomingFileInfo.broadcast,
incomingFileInfo.peer_id
);
receiveFileInfo.innerText = fileToReceiveInfo;
elemDisplay(receiveFileDiv, true);
receiveProgress.max = incomingFileInfo.file.fileSize;
receiveInProgress = true;
userLog('toast', fileToReceiveInfo);
}
/**
* The file will be saved in the Blob. You will be asked to confirm if you want to save it on your PC / Mobile device.
* https://developer.mozilla.org/en-US/docs/Web/API/Blob
*/
function endDownload() {
playSound('download');
// save received file into Blob
const blob = new Blob(incomingFileData);
const file = incomingFileInfo.file.fileName;
incomingFileData = [];
// if file is image, show the preview
if (isImageURL(incomingFileInfo.file.fileName)) {
const reader = new FileReader();
reader.onload = (e) => {
Swal.fire({
allowOutsideClick: false,
background: swBg,
position: 'center',
title: 'Received file',
text: incomingFileInfo.file.fileName + ' size ' + bytesToSize(incomingFileInfo.file.fileSize),
imageUrl: e.target.result,
imageAlt: 'mirotalk-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) saveBlobToFile(blob, file);
});
};
// blob where is stored downloaded file
reader.readAsDataURL(blob);
} else {
// not img file
Swal.fire({
allowOutsideClick: false,
background: swBg,
imageAlt: 'mirotalk-file-download',
imageUrl: images.share,
position: 'center',
title: 'Received file',
text: incomingFileInfo.file.fileName + ' size ' + bytesToSize(incomingFileInfo.file.fileSize),
showDenyButton: true,
confirmButtonText: `Save`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) saveBlobToFile(blob, file);
});
}
}
/**
* Save to PC / Mobile devices
* https://developer.mozilla.org/en-US/docs/Web/API/Blob
* @param {object} blob content
* @param {string} file to save
*/
function saveBlobToFile(blob, file) {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
elemDisplay(a, false);
a.href = url;
a.download = file;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
/**
* Opend and send Video URL to all peers in the room
* @param {string} peer_id socket.id
*/
function sendVideoUrl(peer_id = null) {
playSound('newMessage');
Swal.fire({
background: swBg,
position: 'center',
imageUrl: images.vaShare,
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 (!thereArePeerConnections()) {
return userLog('info', 'No participants detected');
}
console.log('Video URL: ' + result.value);
/*
https://www.youtube.com/watch?v=RT6_Id5-7-s
http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3
*/
if (!isVideoTypeSupported(result.value)) {
return userLog('warning', 'Something wrong, try with another Video or audio URL');
}
const is_youtube = getVideoType(result.value) == 'na' ? true : false;
const video_url = is_youtube ? getYoutubeEmbed(result.value) : result.value;
const config = {
peer_id: peer_id,
video_src: video_url,
};
openVideoUrlPlayer(config);
emitVideoPlayer('open', config);
}
});
// Take URL from clipboard ex:
// https://www.youtube.com/watch?v=1ZYbU82GVz4
navigator.clipboard
.readText()
.then((clipboardText) => {
if (!clipboardText) return false;
const sanitizedText = filterXSS(clipboardText);
const inputElement = Swal.getInput();
if (isVideoTypeSupported(sanitizedText) && inputElement) {
inputElement.value = sanitizedText;
}
return false;
})
.catch(() => {
return false;
});
}
/**
* Open video url Player
*/
function openVideoUrlPlayer(config) {
console.log('Open video Player', config);
const videoSrc = config.video_src;
const videoType = getVideoType(videoSrc);
const videoEmbed = getYoutubeEmbed(videoSrc);
console.log('Video embed', videoEmbed);
//
if (!isVideoUrlPlayerOpen) {
if (videoEmbed) {
playSound('newMessage');
console.log('Load element type: iframe');
videoUrlIframe.src = videoEmbed;
elemDisplay(videoUrlCont, true, 'flex');
isVideoUrlPlayerOpen = true;
} else {
playSound('newMessage');
console.log('Load element type: Video');
elemDisplay(videoAudioUrlCont, true, 'flex');
videoAudioUrlElement.setAttribute('src', videoSrc);
videoAudioUrlElement.type = videoType;
if (videoAudioUrlElement.type == 'video/mp3') {
videoAudioUrlElement.poster = images.audioGif;
}
isVideoUrlPlayerOpen = true;
}
} else {
// video player seems open
if (videoEmbed) {
videoUrlIframe.src = videoEmbed;
} else {
videoAudioUrlElement.src = videoSrc;
}
}
}
/**
* Get video type
* @param {string} url
* @returns string video type
*/
function 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';
}
/**
* Check if video URL is supported
* @param {string} url
* @returns boolean true/false
*/
function isVideoTypeSupported(url) {
if (
url.endsWith('.mp4') ||
url.endsWith('.mp3') ||
url.endsWith('.webm') ||
url.endsWith('.ogg') ||
url.includes('youtube.com')
)
return true;
return false;
}
/**
* Get youtube embed URL
* @param {string} url of YouTube video
* @returns {string} YouTube Embed URL
*/
function getYoutubeEmbed(url) {
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
const match = url.match(regExp);
return match && match[7].length == 11 ? 'https://www.youtube.com/embed/' + match[7] + '?autoplay=1' : false;
}
/**
* Close Video Url Player
*/
function closeVideoUrlPlayer() {
console.log('Close video Player', {
videoUrlIframe: videoUrlIframe.src,
videoAudioUrlElement: videoAudioUrlElement.src,
});
if (videoUrlIframe.src != '') videoUrlIframe.setAttribute('src', '');
if (videoAudioUrlElement.src != '') videoAudioUrlElement.setAttribute('src', '');
elemDisplay(videoUrlCont, false);
elemDisplay(videoAudioUrlCont, false);
isVideoUrlPlayerOpen = false;
}
/**
* Emit video palyer to peers
* @param {string} video_action type
* @param {object} config data
*/
function emitVideoPlayer(video_action, config = {}) {
sendToServer('videoPlayer', {
room_id: roomId,
peer_name: myPeerName,
video_action: video_action,
video_src: config.video_src,
peer_id: config.peer_id,
});
}
/**
* Handle Video Player
* @param {object} config data
*/
function handleVideoPlayer(config) {
const { peer_name, video_action } = config;
//
switch (video_action) {
case 'open':
userLog('toast', `${icons.user} ${peer_name} \n open video player`);
openVideoUrlPlayer(config);
break;
case 'close':
userLog('toast', `${icons.user} ${peer_name} \n close video player`);
closeVideoUrlPlayer();
break;
default:
break;
}
}
/**
* Handle peer kick out event button
* @param {string} peer_id socket.id
*/
function handlePeerKickOutBtn(peer_id) {
if (!buttons.remote.showKickOutBtn) return;
const peerKickOutBtn = getId(peer_id + '_kickOut');
peerKickOutBtn.addEventListener('click', (e) => {
isPresenter
? kickOut(peer_id)
: msgPopup('warning', 'Only the presenter can eject participants', 'top-end', 4000);
});
}
/**
* Eject peer, confirm before
* @param {string} peer_id socket.id
*/
function kickOut(peer_id) {
const pName = getId(peer_id + '_name').innerText;
Swal.fire({
background: swBg,
position: 'center',
imageUrl: images.confirmation,
title: 'Kick out ' + pName,
text: 'Are you sure you want to kick out this participant?',
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
// send peer to kick out from room
sendToServer('kickOut', {
room_id: roomId,
peer_id: peer_id,
peer_uuid: myPeerUUID,
peer_name: myPeerName,
});
}
});
}
/**
* Start caption if not already started
* @param {object} config data
*/
function handleCaptionActions(config) {
const { peer_name, action } = config;
switch (action) {
case 'start':
if (!speechRecognition) {
userLog(
'info',
`${peer_name} wants to start captions for this session, but your browser does not support it. Please use a Chromium-based browser like Google Chrome, Microsoft Edge, or Brave.`
);
return;
}
if (recognitionRunning || !buttons.main.showCaptionRoomBtn) return;
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: true,
background: swBg,
imageUrl: images.caption,
title: 'Start Captions',
text: `${peer_name} wants to start the captions for this session. Would you like to enable them?`,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
if (!isCaptionBoxVisible) {
captionBtn.click();
}
if (!recognitionRunning) {
const { recognitionLanguageIndex, recognitionDialectIndex } = config.data;
recognitionLanguage.selectedIndex = recognitionLanguageIndex;
updateCountry();
recognitionDialect.selectedIndex = recognitionDialectIndex;
speechRecognitionStart.click();
}
}
});
break;
default:
break;
}
}
/**
* You will be kicked out from the room and popup the peer name that performed this action
* @param {object} config data
*/
function handleKickedOut(config) {
signalingSocket.disconnect();
const { peer_name } = config;
playSound('eject');
let timerInterval;
Swal.fire({
allowOutsideClick: false,
background: swBg,
position: 'center',
imageUrl: images.leave,
title: 'Kicked out!',
html:
`<h2 style="color: #FF2D00;">` +
`User ` +
peer_name +
`</h2> will kick out you after <b style="color: #FF2D00;"></b> milliseconds.`,
timer: 5000,
timerProgressBar: true,
didOpen: () => {
Swal.showLoading();
timerInterval = setInterval(() => {
const content = Swal.getHtmlContainer();
if (content) {
const b = content.querySelector('b');
if (b) b.textContent = Swal.getTimerLeft();
}
}, 100);
},
willClose: () => {
clearInterval(timerInterval);
},
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then(() => {
checkRecording();
openURL('/newcall');
});
}
/**
* MiroTalk about info
*/
function showAbout() {
playSound('newMessage');
Swal.fire({
background: swBg,
position: 'center',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.5.48',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: `
<br/>
<div id="about">
${
brand.about?.html && brand.about.html.trim() !== ''
? brand.about.html
: `
<button
id="support-button"
data-umami-event="Support button"
onclick="window.open('https://codecanyon.net/user/miroslavpejic85', '_blank')">
<i class="${className.heart}"></i>&nbsp;Support
</button>
<br /><br /><br />
Author:
<a
id="linkedin-button"
data-umami-event="Linkedin button"
href="https://www.linkedin.com/in/miroslav-pejic-976a07101/"
target="_blank">
Miroslav Pejic
</a>
<br /><br />
Email:
<a
id="email-button"
data-umami-event="Email button"
href="mailto:miroslav.pejic.85@gmail.com?subject=MiroTalk P2P info">
miroslav.pejic.85@gmail.com
</a>
<br /><br />
<hr />
<span>&copy; 2025 MiroTalk P2P, all rights reserved</span>
<hr />
`
}
</div>
`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
/**
* Leave the Room and create a new one
*/
function leaveRoom() {
checkRecording();
surveyActive ? leaveFeedback() : redirectOnLeave();
}
/**
* Ask for feedback when room exit
*/
function leaveFeedback() {
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: true,
showCancelButton: true,
confirmButtonColor: 'green',
denyButtonColor: 'red',
cancelButtonColor: 'gray',
background: swBg,
imageUrl: images.feedback,
position: 'top',
title: 'Leave a feedback',
text: 'Do you want to rate your MiroTalk experience?',
confirmButtonText: `Yes`,
denyButtonText: `No`,
cancelButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
openURL(surveyURL);
} else if (result.isDenied) {
redirectOnLeave();
}
});
}
function redirectOnLeave() {
redirectActive ? openURL(redirectURL) : openURL('/newcall');
}
/**
* Make Obj draggable: https://www.w3schools.com/howto/howto_js_draggable.asp
* @param {object} elmnt father element
* @param {object} dragObj children element to make father draggable (click + mouse move)
*/
function dragElement(elmnt, dragObj) {
let pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
if (dragObj) {
// if present, the header is where you move the DIV from:
dragObj.onmousedown = dragMouseDown;
} else {
// otherwise, move the DIV from anywhere inside the DIV:
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = elmnt.offsetTop - pos2 + 'px';
elmnt.style.left = elmnt.offsetLeft - pos1 + 'px';
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
}
/**
* Make Obj Undraggable
* @param {object} elmnt father element
* @param {object} dragObj children element to make father undraggable
*/
function undragElement(elmnt, dragObj) {
if (dragObj) {
dragObj.onmousedown = null;
} else {
elmnt.onmousedown = null;
}
elmnt.style.top = '';
elmnt.style.left = '';
}
/**
* Date Format: https://convertio.co/it/
* @returns {string} date string format: DD-MM-YYYY-H_M_S
*/
function getDataTimeString() {
const d = new Date();
const date = d.toISOString().split('T')[0];
const time = d.toTimeString().split(' ')[0];
return `${date}-${time}`;
}
/**
* Convert bytes to KB-MB-GB-TB
* @param {object} bytes to convert
* @returns {string} converted size
*/
function bytesToSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0 Byte';
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
}
/**
* Handle peer audio volume
* @param {object} data peer audio
*/
function handlePeerVolume(data) {
const { peer_id, volume } = data;
let audioColorTmp = '#19bb5c';
if ([50, 60, 70].includes(volume)) audioColorTmp = '#FFA500'; // Orange
if ([80, 90, 100].includes(volume)) audioColorTmp = '#FF0000'; // Red
if (!isAudioPitchBar) {
const remotePeerAvatarImg = getId(peer_id + '_avatar');
if (remotePeerAvatarImg) {
applyBoxShadowEffect(remotePeerAvatarImg, audioColorTmp, 100);
}
const remotePeerVideo = getId(peer_id + '___video');
if (remotePeerVideo && remotePeerVideo.classList.contains('videoCircle')) {
applyBoxShadowEffect(remotePeerVideo, audioColorTmp, 100);
}
return;
}
const remotePitchBar = getId(peer_id + '_pitch_bar');
//let remoteVideoWrap = getId(peer_id + '_videoWrap');
if (!remotePitchBar) return;
remotePitchBar.style.backgroundColor = audioColorTmp;
remotePitchBar.style.height = volume + '%';
//remoteVideoWrap.classList.toggle('speaking');
setTimeout(function () {
remotePitchBar.style.backgroundColor = '#19bb5c';
remotePitchBar.style.height = '0%';
//remoteVideoWrap.classList.toggle('speaking');
}, 100);
}
/**
* Handle my audio volume
* @param {object} data my audio
*/
function handleMyVolume(data) {
const { volume } = data;
let audioColorTmp = '#19bb5c';
if ([50, 60, 70].includes(volume)) audioColorTmp = '#FFA500'; // Orange
if ([80, 90, 100].includes(volume)) audioColorTmp = '#FF0000'; // Red
if (!isAudioPitchBar || !myPitchBar) {
const localPeerAvatarImg = getId('myVideoAvatarImage');
if (localPeerAvatarImg) {
applyBoxShadowEffect(localPeerAvatarImg, audioColorTmp, 100);
}
if (myVideo && myVideo.classList.contains('videoCircle')) {
applyBoxShadowEffect(myVideo, audioColorTmp, 100);
}
return;
}
myPitchBar.style.backgroundColor = audioColorTmp;
myPitchBar.style.height = volume + '%';
//myVideoWrap.classList.toggle('speaking');
setTimeout(function () {
myPitchBar.style.backgroundColor = '#19bb5c';
myPitchBar.style.height = '0%';
//myVideoWrap.classList.toggle('speaking');
}, 100);
}
/**
* Apply Box Shadow effect to element
* @param {object} element
* @param {string} color
* @param {integer} delay ms
*/
function applyBoxShadowEffect(element, color, delay = 200) {
if (element) {
element.style.boxShadow = `0 0 20px ${color}`;
setTimeout(() => {
element.style.boxShadow = 'none';
}, delay);
}
}
/**
* Basic user logging using https://sweetalert2.github.io & https://animate.style/
* @param {string} type of popup
* @param {string} message to popup
* @param {integer} timer toast duration ms
*/
function userLog(type, message, timer = 3000) {
switch (type) {
case 'warning':
case 'error':
Swal.fire({
background: swBg,
position: 'center',
icon: type,
title: type,
text: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
playSound('alert');
break;
case 'info':
case 'success':
Swal.fire({
background: swBg,
position: 'center',
icon: type,
title: type,
text: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
break;
case 'success-html':
Swal.fire({
background: swBg,
position: 'center',
icon: 'success',
title: 'Success',
html: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
break;
case 'toast':
const Toast = Swal.mixin({
background: swBg,
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: timer,
timerProgressBar: true,
});
Toast.fire({
icon: 'info',
html: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
break;
// ......
default:
alert(message);
break;
}
}
/**
* Popup Toast message
* @param {string} icon info, success, alert, warning
* @param {string} title message title
* @param {string} html message in html format
* @param {string} position message position
* @param {integer} duration time popup in ms
*/
function toastMessage(icon, title, html, position = 'top-end', duration = 3000) {
if (['warning', 'error'].includes(icon)) playSound('alert');
const Toast = Swal.mixin({
background: swBg,
position: position,
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' },
});
}
/**
* Popup html message
* @param {string} icon info, success, alert, warning
* @param {string} imageUrl image path
* @param {string} title message title
* @param {string} html message in html format
* @param {string} position message position
* @param {string} redirectURL if set on press ok will be redirected to the URL
*/
function msgHTML(icon, imageUrl, title, html, position = 'center', redirectURL = false) {
if (['warning', 'error'].includes(icon)) playSound('alert');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
position: position,
icon: icon,
imageUrl: imageUrl,
title: title,
html: html,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed && redirectURL) {
openURL(redirectURL);
}
});
}
/**
* Message popup
* @param {string} icon info, success, warning, error
* @param {string} message to show
* @param {string} position of the toast
* @param {integer} timer ms before to hide
*/
function msgPopup(icon, message, position, timer = 1000) {
if (['warning', 'error'].includes(icon)) playSound('alert');
const Toast = Swal.mixin({
background: swBg,
toast: true,
position: position,
showConfirmButton: false,
timer: timer,
timerProgressBar: true,
});
Toast.fire({
icon: icon,
title: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
/**
* https://notificationsounds.com/notification-sounds
* @param {string} name audio to play
* @param {boolean} force audio
* @param {string} path of sound files
*/
async function playSound(name, force = false, path = '../sounds/') {
if (!notifyBySound && !force) return;
const sound = path + name + '.mp3';
const audioToPlay = new Audio(sound);
try {
audioToPlay.volume = 0.5;
await audioToPlay.play();
} catch (err) {
// console.error("Cannot play sound", err);
// Automatic playback failed. (safari)
return;
}
}
/**
* Open specified URL
* @param {string} url to open
* @param {boolean} blank if true opne url in the new tab
*/
function openURL(url, blank = false) {
blank ? window.open(url, '_blank') : (window.location.href = url);
}
/**
* Show-Hide all elements grp by class name
* @param {string} className to toggle
* @param {string} displayState of the element
*/
function toggleClassElements(className, displayState) {
const elements = getEcN(className);
for (let i = 0; i < elements.length; i++) {
elements[i].style.display = displayState;
}
}
/**
* Check if valid filename
* @param {string} fileName
* @returns boolean
*/
function isValidFileName(fileName) {
const invalidChars = /[\\\/\?\*\|:"<>]/;
return !invalidChars.test(fileName);
}
/**
* Check if WebRTC supported
* @return {boolean} true/false
*/
function checkWebRTCSupported() {
return !!(navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function');
}
/**
* Get Html element by Id
* @param {string} id of the element
* @returns {object} element
*/
function getId(id) {
return document.getElementById(id);
}
/**
* Get all element descendants of node
* @param {string} selectors
* @returns all element descendants of node that match selectors.
*/
function getQsA(selectors) {
return document.querySelectorAll(selectors);
}
/**
* Get element by selector
* @param {string} selector
* @returns element
*/
function getQs(selector) {
return document.querySelector(selector);
}
/**
* Set document style property
* @param {string} key
* @param {string} value
* @returns {objects} element
*/
function setSP(key, value) {
return document.documentElement.style.setProperty(key, value);
}
/**
* Get Html element by selector
* @param {string} selector of the element
* @returns {object} element
*/
function getSl(selector) {
return document.querySelector(selector);
}
/**
* Get ALL Html elements by selector
* @param {string} selector of the element
* @returns {object} element
*/
function getSlALL(selector) {
return document.querySelectorAll(selector);
}
/**
* Get Html element by class name
* @param {string} className of the element
* @returns {object} element
*/
function getEcN(className) {
return document.getElementsByClassName(className);
}
/**
* Get html element by name
* @param {string} name
* @returns element
*/
function getName(name) {
return document.getElementsByName(name);
}
/**
* Element style display
* @param {object} elem
* @param {boolean} yes true/false
*/
function elemDisplay(element, display, mode = 'inline') {
element.style.display = display ? mode : 'none';
}
/**
* Sanitize XSS scripts
* @param {object} src object
* @returns sanitized object
*/
function sanitizeXSS(src) {
return JSON.parse(filterXSS(JSON.stringify(src)));
}
/**
* Disable element
* @param {object} elem
* @param {boolean} disabled
*/
function disable(elem, disabled) {
elem.disabled = disabled;
}
/**
* Sleep in ms
* @param {integer} ms milleseconds
* @returns Promise
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}