Files
mirotalk/public/js/client.js
T

9646 lines
319 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.2.4
*
*/
'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 = {
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',
}; // 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',
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',
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',
};
// 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>',
};
// Whiteboard and fileSharing
const fileSharingInput = '*'; // allow all file extensions
const Base64Prefix = 'data:application/pdf;base64,';
const wbPdfInput = 'application/pdf';
const wbImageInput = 'image/*';
const wbWidth = 1200;
const wbHeight = 600;
// Peer infos
const userAgent = navigator.userAgent.toLowerCase();
const detectRtcVersion = DetectRTC.version;
const isWebRTCSupported = DetectRTC.isWebRTCSupported;
const isMobileDevice = DetectRTC.isMobileDevice;
const isTabletDevice = isTablet(userAgent);
const isIPadDevice = isIpad(userAgent);
const isDesktopDevice = !isMobileDevice && !isTabletDevice && !isIPadDevice;
const osName = DetectRTC.osName;
const osVersion = DetectRTC.osVersion;
const browserName = DetectRTC.browser.name;
const browserVersion = DetectRTC.browser.version;
const peerInfo = getPeerInfo();
// 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 = !isMobileDevice && document.pictureInPictureEnabled;
// Check if Document PIP is supported by this browser
const showDocumentPipBtn = !isMobileDevice && !isEmbedded && 'documentPictureInPicture' in window;
/**
* Configuration for controlling the visibility of buttons in the MiroTalk P2P client.
* Set properties to true to show the corresponding buttons, or false to hide them.
* captionBtn, showSwapCameraBtn, showScreenShareBtn, showFullScreenBtn, showVideoPipBtn, showDocumentPipBtn -> (auto-detected).
*/
const buttons = {
main: {
showShareRoomBtn: true,
showHideMeBtn: true,
showAudioBtn: true,
showVideoBtn: true,
showScreenBtn: true,
showRecordStreamBtn: true,
showChatRoomBtn: true,
showCaptionRoomBtn: true,
showRoomEmojiPickerBtn: true,
showMyHandBtn: true,
showWhiteboardBtn: true,
showFileShareBtn: true,
showDocumentPipBtn: showDocumentPipBtn,
showMySettingsBtn: true,
showAboutBtn: true, // Please keep me always true, Thank you!
},
chat: {
showMaxBtn: true,
showSaveMessageBtn: true,
showMarkDownBtn: true,
showChatGPTBtn: true,
showFileShareBtn: true,
showShareVideoAudioBtn: true,
showParticipantsBtn: true,
},
caption: {
showMaxBtn: true,
},
settings: {
showMicOptionsBtn: true,
showTabRoomPeerName: true,
showTabRoomParticipants: true,
showTabRoomSecurity: true,
showMuteEveryoneBtn: true,
showHideEveryoneBtn: true,
showEjectEveryoneBtn: true,
showLockRoomBtn: true,
showUnlockRoomBtn: true,
},
remote: {
showAudioVolume: true,
audioBtnClickAllowed: true,
videoBtnClickAllowed: true,
showKickOutBtn: true,
showSnapShotBtn: true,
showFileShareBtn: true,
showShareVideoAudioBtn: true,
showPrivateMessageBtn: true,
showZoomInOutBtn: false,
showVideoPipBtn: showVideoPipBtn,
},
local: {
showSnapShotBtn: true,
showVideoCircleBtn: true,
showZoomInOutBtn: false,
showVideoPipBtn: showVideoPipBtn,
},
whiteboard: {
whiteboardLockBtn: false,
},
};
// Loading div
const loadingDiv = getId('loadingDiv');
// Video/Audio media container
const videoMediaContainer = getId('videoMediaContainer');
const videoPinMediaContainer = getId('videoPinMediaContainer');
const audioMediaContainer = getId('audioMediaContainer');
// Init audio-video
const initUser = getId('initUser');
const initVideo = getId('initVideo');
const initVideoBtn = getId('initVideoBtn');
const initAudioBtn = getId('initAudioBtn');
const initScreenShareBtn = getId('initScreenShareBtn');
const initVideoSelect = getId('initVideoSelect');
const initMicrophoneSelect = getId('initMicrophoneSelect');
const initSpeakerSelect = getId('initSpeakerSelect');
// Buttons bar
const buttonsBar = getId('buttonsBar');
const shareRoomBtn = getId('shareRoomBtn');
const hideMeBtn = getId('hideMeBtn');
const videoBtn = getId('videoBtn');
const swapCameraBtn = getId('swapCameraBtn');
const audioBtn = getId('audioBtn');
const screenShareBtn = getId('screenShareBtn');
const recordStreamBtn = getId('recordStreamBtn');
const fullScreenBtn = getId('fullScreenBtn');
const chatRoomBtn = getId('chatRoomBtn');
const captionBtn = getId('captionBtn');
const roomEmojiPickerBtn = getId('roomEmojiPickerBtn');
const myHandBtn = getId('myHandBtn');
const whiteboardBtn = getId('whiteboardBtn');
const fileShareBtn = getId('fileShareBtn');
const documentPiPBtn = getId('documentPiPBtn');
const mySettingsBtn = getId('mySettingsBtn');
const aboutBtn = getId('aboutBtn');
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 msgerTheme = getId('msgerTheme');
const msgerCPBtn = getId('msgerCPBtn');
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');
// Caption section
const captionDraggable = getId('captionDraggable');
const captionHeader = getId('captionHeader');
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');
// 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 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 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 pauseRecBtn = getId('pauseRecBtn');
const resumeRecBtn = getId('resumeRecBtn');
const recordingTime = getId('recordingTime');
const lastRecordingInfo = getId('lastRecordingInfo');
const themeSelect = getId('mirotalkTheme');
const videoObjFitSelect = getId('videoObjFitSelect');
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 isPeerPresenter = getId('isPeerPresenter');
const peersCount = getId('peersCount');
const screenFpsDiv = getId('screenFpsDiv');
// Audio options
const dropDownMicOptions = getId('dropDownMicOptions');
const switchAutoGainControl = getId('switchAutoGainControl');
const switchNoiseSuppression = getId('switchNoiseSuppression');
const switchEchoCancellation = getId('switchEchoCancellation');
const sampleRateSelect = getId('sampleRateSelect');
const sampleSizeSelect = getId('sampleSizeSelect');
const channelCountSelect = getId('channelCountSelect');
const micLatencyRange = getId('micLatencyRange');
const micVolumeRange = getId('micVolumeRange');
const applyAudioOptionsBtn = getId('applyAudioOptionsBtn');
const micOptionsBtn = getId('micOptionsBtn');
const micDropDownMenu = getSl('.dropdown-menu');
const micLatencyValue = getId('micLatencyValue');
const micVolumeValue = getId('micVolumeValue');
// 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 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 whiteboardCloseBtn = getId('whiteboardCloseBtn');
// Room actions buttons
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');
// 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');
//....
const userLimits = {
active: false, // Limit users per room
count: 2, // Limit 2 users per room if userLimits.active true
};
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 4k 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;
// misc
let swBg = 'rgba(0, 0, 0, 0.7)'; // swAlert background color
let callElapsedTime; // count time
let mySessionTime; // conference session time
let isDocumentOnFullScreen = false;
// peer
let myPeerId; // This socket.id
let myPeerUUID = getUUID(); // Unique peer id
let myPeerName = getPeerName();
let myUsername = window.sessionStorage.peer_username ? window.sessionStorage.peer_username : getPeerUsername(); // default false if not passed by query params
let myPassword = window.sessionStorage.peer_password ? window.sessionStorage.peer_password : getPeerPassword(); // default false if not passed by query params
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 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 buttons
let mainButtonsBarPosition = 'vertical'; // vertical - horizontal
let placement = 'right'; // https://atomiks.github.io/tippyjs/#placements
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 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
// 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 isAudioPitchBar = true;
let isPushToTalkActive = false;
let isSpaceDown = false;
// recording
let mediaRecorder;
let recordedBlobs;
let audioRecorder; // helpers.js
let recScreenStream; // screen media to recording
let recTimer;
let recElapsedTime;
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';
/**
* 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');
// 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(msgerTheme, 'Ghost theme', 'bottom');
setTippy(msgerClean, 'Clean the messages', 'bottom');
setTippy(msgerSaveBtn, 'Save the messages', '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(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(
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(recImage, 'Toggle recording', 'right');
// Whiteboard buttons
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');
setTippy(whiteboardImgFileBtn, 'Add image from file', 'bottom');
setTippy(whiteboardPdfFileBtn, 'Add pdf from file', 'bottom');
setTippy(whiteboardImgUrlBtn, 'Add image from URL', 'bottom');
setTippy(whiteboardTextBtn, 'Add the text', 'bottom');
setTippy(whiteboardLineBtn, 'Add the line', 'bottom');
setTippy(whiteboardRectBtn, 'Add the rectangle', 'bottom');
setTippy(whiteboardTriangleBtn, 'Add triangle', 'bottom');
setTippy(whiteboardCircleBtn, 'Add the circle', 'bottom');
setTippy(whiteboardSaveBtn, 'Save the board', 'bottom');
setTippy(whiteboardEraserBtn, 'Erase the object', 'bottom');
setTippy(whiteboardCleanBtn, 'Clean the board', 'bottom');
setTippy(whiteboardLockBtn, 'If enabled, participants cannot interact', 'right');
setTippy(whiteboardCloseBtn, 'Close', 'right');
// Suspend/Hide File transfer buttons
setTippy(sendAbortBtn, 'Abort file transfer', 'right-start');
setTippy(receiveHideBtn, 'Hide file transfer', 'right-start');
// 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;
// main buttons
placement = btnsBarSelect.options[btnsBarSelect.selectedIndex].value == 'vertical' ? 'right' : 'top';
setTippy(shareRoomBtn, 'Invite others to join', placement);
setTippy(hideMeBtn, 'Toggle hide myself from the room view', placement);
setTippy(audioBtn, useAudio ? 'Stop the audio' : 'My audio is disabled', placement);
setTippy(videoBtn, useVideo ? 'Stop the video' : 'My video is disabled', placement);
setTippy(screenShareBtn, 'Start screen sharing', placement);
setTippy(recordStreamBtn, 'Start recording', placement);
setTippy(fullScreenBtn, 'View full screen', placement);
setTippy(chatRoomBtn, 'Open the chat', placement);
setTippy(captionBtn, 'Open the caption', placement);
setTippy(roomEmojiPickerBtn, 'Send reaction', placement);
setTippy(myHandBtn, 'Raise your hand', placement);
setTippy(whiteboardBtn, 'Open the whiteboard', placement);
setTippy(fileShareBtn, 'Share file', placement);
setTippy(documentPiPBtn, 'Toggle picture in picture', placement);
setTippy(mySettingsBtn, 'Open the settings', placement);
setTippy(aboutBtn, 'About this project', placement);
setTippy(leaveRoomBtn, 'Leave this room', placement);
}
/**
* 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();
}
tippy(element, {
content: content,
placement: placement,
});
} else {
console.warn('setTippy element not found with content', content);
}
}
/**
* Get peer info using DetectRTC
* https://github.com/muaz-khan/DetectRTC
* @returns {object} peer info
*/
function getPeerInfo() {
return {
detectRTCversion: detectRtcVersion,
isWebRTCSupported: isWebRTCSupported,
isDesktopDevice: isDesktopDevice,
isMobileDevice: isMobileDevice,
isTabletDevice: isTabletDevice,
isIPadDevice: isIPadDevice,
osName: osName,
osVersion: osVersion,
browserName: browserName,
browserVersion: browserVersion,
};
}
/**
* 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.substring(6);
// 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 username
* @returns {mixed} boolean false or username string
*/
function getPeerUsername() {
let qs = new URLSearchParams(window.location.search);
let username = filterXSS(qs.get('username'));
let queryUsername = false;
if (username) {
queryUsername = username;
}
console.log('Direct join', { username: queryUsername });
return queryUsername;
}
/**
* Get Peer password
* @returns {mixed} boolean false or password string
*/
function getPeerPassword() {
let qs = new URLSearchParams(window.location.search);
let password = filterXSS(qs.get('password'));
let queryPassword = false;
if (password) {
queryPassword = password;
}
console.log('Direct join', { password: queryPassword });
return queryPassword;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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('message', handleMessage);
signalingSocket.on('wbCanvasToJson', handleJsonToWbCanvas);
signalingSocket.on('whiteboardAction', handleWhiteboardAction);
signalingSocket.on('kickOut', handleKickedOut);
signalingSocket.on('fileInfo', handleFileInfo);
signalingSocket.on('fileAbort', handleFileAbort);
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 + ' ]');
if (localVideoMediaStream && localAudioMediaStream) {
await joinToChannel();
} else {
await initEnumerateDevices();
await setupLocalVideoMedia();
await setupLocalAudioMedia();
if (!useVideo || (!useVideo && !useAudio)) {
await loadLocalMedia(new MediaStream(), 'video');
}
getHtmlElementsById();
setButtonsToolTip();
manageLeftButtons();
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 } = 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 (userLimits.active && peers_count > userLimits.count) {
return roomIsBusy();
}
// Let start with some basic rules
isPresenter = isPeerReconnected ? isPresenter : is_presenter;
isPeerPresenter.innerText = isPresenter;
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: 'Oops, Unauthorized',
text: 'The host has user authentication enabled',
confirmButtonText: `Login`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then(() => {
// Login required to join room
openURL(`/login/?room=${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 ${userLimits.count} 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.showMicOptionsBtn = false;
buttons.settings.showTabRoomParticipants = false;
buttons.settings.showTabRoomSecurity = false;
buttons.remote.audioBtnClickAllowed = false;
buttons.remote.videoBtnClickAllowed = false;
buttons.remote.showKickOutBtn = false;
buttons.whiteboard.whiteboardLockBtn = false;
//...
} else {
buttons.settings.showMicOptionsBtn = true;
buttons.settings.showTabRoomParticipants = true;
buttons.settings.showTabRoomSecurity = 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(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(fileShareBtn, buttons.main.showFileShareBtn);
elemDisplay(documentPiPBtn, buttons.main.showDocumentPipBtn);
elemDisplay(mySettingsBtn, buttons.main.showMySettingsBtn);
elemDisplay(aboutBtn, buttons.main.showAboutBtn);
// chat
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(captionMaxBtn, !isMobileDevice && buttons.caption.showMaxBtn);
// Settings
elemDisplay(dropDownMicOptions, buttons.settings.showMicOptionsBtn && isPresenter); // auto-detected
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);
// Whiteboard
buttons.whiteboard.whiteboardLockBtn
? elemDisplay(whiteboardLockBtn, true)
: elemDisplay(whiteboardLockBtn, false, 'flex');
}
/**
* set your name for the conference
*/
async function whoAreYou() {
console.log('11. Who are you?');
elemDisplay(loadingDiv, false);
document.body.style.background = 'var(--body-bg)';
if (myPeerName) {
myPeerName = filterXSS(myPeerName);
console.log(`11.1 Check if ${myPeerName} exist in the room`, roomId);
if (await checkUserName()) {
return userNameAlreadyInRoom();
}
checkPeerAudioVideo();
whoAreYouJoin();
playSound('addPeer');
return;
}
playSound('newMessage');
await loadLocalStorage();
if (!useVideo || !buttons.main.showVideoBtn) {
useVideo = false;
elemDisplay(document.getElementById('initVideo'), false);
elemDisplay(document.getElementById('initVideoBtn'), 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');
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
background: swBg,
title: 'MiroTalk P2P',
position: 'center',
input: 'text',
inputPlaceholder: 'Enter your name',
inputAttributes: { maxlength: 32 },
inputValue: window.localStorage.peer_name ? window.localStorage.peer_name : '',
html: initUser, // inject html
confirmButtonText: `Join meeting`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
inputValidator: async (value) => {
if (!value) return 'Please enter your name';
// 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 {
window.localStorage.peer_name = myPeerName;
whoAreYouJoin();
}
},
}).then(() => {
playSound('addPeer');
});
// select video - audio
initVideoSelect.onchange = async () => {
videoSelect.selectedIndex = initVideoSelect.selectedIndex;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
await changeInitCamera(initVideoSelect.value);
await handleLocalCameraMirror();
};
initMicrophoneSelect.onchange = async () => {
audioInputSelect.selectedIndex = initMicrophoneSelect.selectedIndex;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value);
await changeLocalMicrophone(audioInputSelect.value);
};
initSpeakerSelect.onchange = () => {
audioOutputSelect.selectedIndex = initSpeakerSelect.selectedIndex;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, audioOutputSelect.selectedIndex, audioOutputSelect.value);
changeAudioDestination();
};
// 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');
}
/**
* 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) {
//
initMicrophoneSelect.selectedIndex = localStorageDevices.audio.index;
initSpeakerSelect.selectedIndex = localStorageDevices.speaker.index;
initVideoSelect.selectedIndex = localStorageDevices.video.index;
//
audioInputSelect.selectedIndex = initMicrophoneSelect.selectedIndex;
audioOutputSelect.selectedIndex = initSpeakerSelect.selectedIndex;
videoSelect.selectedIndex = initVideoSelect.selectedIndex;
//
if (lS.DEVICES_COUNT.audio != localStorageDevices.audio.count) {
console.log('12.1 Audio devices seems changed, use default index 0');
initMicrophoneSelect.selectedIndex = 0;
audioInputSelect.selectedIndex = 0;
lS.setLocalStorageDevices(
lS.MEDIA_TYPE.audio,
initMicrophoneSelect.selectedIndex,
initMicrophoneSelect.value,
);
}
if (lS.DEVICES_COUNT.speaker != localStorageDevices.speaker.count) {
console.log('12.2 Speaker devices seems changed, use default index 0');
initSpeakerSelect.selectedIndex = 0;
audioOutputSelect.selectedIndex = 0;
lS.setLocalStorageDevices(
lS.MEDIA_TYPE.speaker,
initSpeakerSelect.selectedIndexIndex,
initSpeakerSelect.value,
);
}
if (lS.DEVICES_COUNT.video != localStorageDevices.video.count) {
console.log('12.3 Video devices seems changed, use default index 0');
initVideoSelect.selectedIndex = 0;
videoSelect.selectedIndex = 0;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, initVideoSelect.selectedIndex, initVideoSelect.value);
}
//
console.log('12.4 Get Local Storage Devices after', lS.getLocalStorageDevices());
}
// Start init cam
if (useVideo && initVideoSelect.value) {
await changeInitCamera(initVideoSelect.value);
await handleLocalCameraMirror();
await checkInitConfig();
}
}
/**
* 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();
}
}
/**
* Change init camera by device id
* @param {string} deviceId
*/
async function changeInitCamera(deviceId) {
if (initStream) {
stopTracks(initStream);
}
// Get video constraints
const videoConstraints = await getVideoConstraints('default');
videoConstraints['deviceId'] = { exact: deviceId };
navigator.mediaDevices
.getUserMedia({ video: videoConstraints })
.then((camStream) => {
// 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());
})
.catch((err) => {
console.error('[Error] changeInitCamera', err);
userLog('error', 'Error while swapping init camera' + err);
initVideoSelect.selectedIndex = 0;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, initVideoSelect.selectedIndex, initVideoSelect.value);
// 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);
navigator.mediaDevices
.getUserMedia({ video: videoConstraints })
.then((camStream) => {
myVideo.srcObject = camStream;
localVideoMediaStream = camStream;
logStreamSettingsInfo('Success attached local video stream', camStream);
refreshMyStreamToPeers(camStream);
setLocalMaxFps(videoMaxFrameRate);
})
.catch((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 = await getAudioConstraints();
audioConstraints['deviceId'] = { exact: deviceId };
console.log('audioConstraints', audioConstraints);
navigator.mediaDevices
.getUserMedia({ audio: audioConstraints })
.then((micStream) => {
myAudio.srcObject = micStream;
localAudioMediaStream = micStream;
logStreamSettingsInfo('Success attached local microphone stream', micStream);
getMicrophoneVolumeIndicator(micStream);
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 });
}
}
/**
* Room and Peer name are ok Join Channel
*/
async function whoAreYouJoin() {
myVideoParagraph.innerText = myPeerName + ' (me)';
setPeerAvatarImgName('myVideoAvatarImage', myPeerName);
setPeerAvatarImgName('myProfileAvatar', myPeerName);
setPeerChatAvatarImgName('right', myPeerName);
joinToChannel();
handleHideMe(isHideMeActive);
}
/**
* 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_username: myUsername,
peer_password: myPassword,
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...
}
/**
* 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,
});
};
}
/**
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onicecandidate
* @param {string} peer_id socket.id
*/
async function handleOnIceCandidate(peer_id) {
peerConnections[peer_id].onicecandidate = (event) => {
if (!event.candidate) return;
sendToServer('relayICE', {
peer_id: peer_id,
ice_candidate: {
sdpMLineIndex: event.candidate.sdpMLineIndex,
candidate: event.candidate.candidate,
},
});
};
}
/**
* 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;
}
} 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;
}
};
};
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';
peerVideoMediaElements[peerVideoId].parentNode.removeChild(peerVideoMediaElements[peerVideoId]);
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) {
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 mirotalk theme | dark | grey | ...
* @param {string} theme type
*/
function setTheme() {
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('--navbar-bg', 'rgba(0, 0, 0, 0.2)');
setSP('--select-bg', '#2c2c2c');
setSP('--tab-btn-active', 'rgb(30 29 29)');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.2)');
setSP('--left-msg-bg', '#252d31');
setSP('--right-msg-bg', '#056162');
setSP('--private-msg-bg', '#6b1226');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
document.body.style.background = 'radial-gradient(#393939, #000000)';
mirotalkTheme.selectedIndex = 0;
break;
case 'grey':
// grey theme
swBg = 'radial-gradient(#666, #333)';
setSP('--body-bg', 'radial-gradient(#666, #333)');
setSP('--msger-bg', 'radial-gradient(#666, #333)');
setSP('--wb-bg', 'radial-gradient(#797979, #000)');
setSP('--navbar-bg', 'rgba(0, 0, 0, 0.2)');
setSP('--select-bg', '#2c2c2c');
setSP('--tab-btn-active', 'rgb(30 29 29)');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.2)');
setSP('--msger-private-bg', 'radial-gradient(#666, #333)');
setSP('--left-msg-bg', '#252d31');
setSP('--right-msg-bg', '#056162');
setSP('--private-msg-bg', '#6b1226');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
document.body.style.background = 'radial-gradient(#666, #333)';
mirotalkTheme.selectedIndex = 1;
break;
case 'green':
// green theme
swBg = 'radial-gradient(#003934, #001E1A)';
setSP('--body-bg', 'radial-gradient(#003934, #001E1A)');
setSP('--msger-bg', 'radial-gradient(#003934, #001E1A)');
setSP('--wb-bg', 'radial-gradient(#003934, #001E1A)');
setSP('--navbar-bg', 'rgba(0, 0, 0, 0.2)');
setSP('--select-bg', '#001E1A');
setSP('--tab-btn-active', '#003934');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.2)');
setSP('--msger-private-bg', 'radial-gradient(#666, #333)');
setSP('--left-msg-bg', '#003934');
setSP('--right-msg-bg', '#001E1A');
setSP('--private-msg-bg', '#6b1226');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
document.body.style.background = 'radial-gradient(#003934, #001E1A)';
mirotalkTheme.selectedIndex = 2;
break;
case 'blue':
// blue theme
swBg = 'radial-gradient(#306bac, #141B41)';
setSP('--body-bg', 'radial-gradient(#306bac, #141B41)');
setSP('--msger-bg', 'radial-gradient(#306bac, #141B41)');
setSP('--wb-bg', 'radial-gradient(#306bac, #141B41)');
setSP('--navbar-bg', 'rgba(0, 0, 0, 0.2)');
setSP('--select-bg', '#141B41');
setSP('--tab-btn-active', '#306bac');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.2)');
setSP('--msger-private-bg', 'radial-gradient(#666, #333)');
setSP('--left-msg-bg', '#306bac');
setSP('--right-msg-bg', '#141B41');
setSP('--private-msg-bg', '#6b1226');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
document.body.style.background = 'radial-gradient(#306bac, #141B41)';
mirotalkTheme.selectedIndex = 3;
break;
case 'red':
// red theme
swBg = 'radial-gradient(#69140E, #3C1518)';
setSP('--body-bg', 'radial-gradient(#69140E, #3C1518)');
setSP('--msger-bg', 'radial-gradient(#69140E, #3C1518)');
setSP('--wb-bg', 'radial-gradient(#69140E, #3C1518)');
setSP('--navbar-bg', 'rgba(0, 0, 0, 0.2)');
setSP('--select-bg', '#3C1518');
setSP('--tab-btn-active', '#69140E');
setSP('--box-shadow', '0px 8px 16px 0px rgba(0, 0, 0, 0.2)');
setSP('--msger-private-bg', 'radial-gradient(#666, #333)');
setSP('--left-msg-bg', '#69140E');
setSP('--right-msg-bg', '#3C1518');
setSP('--private-msg-bg', '#6b1226');
setSP('--btn-bar-bg-color', '#FFFFFF');
setSP('--btn-bar-color', '#000000');
document.body.style.background = 'radial-gradient(#69140E, #3C1518)';
mirotalkTheme.selectedIndex = 4;
break;
// ...
default:
return console.log('No theme found');
}
//setButtonsBarPosition(mainButtonsBarPosition);
}
/**
* Set buttons bar position
* @param {string} position vertical / horizontal
*/
function setButtonsBarPosition(position) {
if (!position || isMobileDevice) return;
mainButtonsBarPosition = position;
switch (mainButtonsBarPosition) {
case 'vertical':
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');
break;
case 'horizontal':
setSP('--btns-top', '95%');
setSP('--btns-right', '25%');
setSP('--btns-left', '50%');
setSP('--btns-margin-left', '-330px');
setSP('--btns-width', '660px');
setSP('--btns-flex-direction', 'row');
break;
default:
console.log('No position found');
break;
}
refreshMainButtonsToolTipPlacement();
}
/**
* Init to enumerate the devices
*/
async function initEnumerateDevices() {
console.log('05. init Enumerate 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((stream) => {
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((stream) => {
enumerateVideoDevices(stream);
useVideo = true;
})
.catch(() => {
useVideo = false;
});
}
/**
* Enumerate Audio
* @param {object} stream
*/
function enumerateAudioDevices(stream) {
console.log('06. Get Audio Devices');
navigator.mediaDevices
.enumerateDevices()
.then((devices) =>
devices.forEach((device) => {
let el,
eli = null;
if ('audioinput' === device.kind) {
el = audioInputSelect;
eli = initMicrophoneSelect;
} else if ('audiooutput' === device.kind) {
el = audioOutputSelect;
eli = initSpeakerSelect;
}
if (!el) return;
addChild(device, [el, eli]);
}),
)
.then(() => {
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
*/
function enumerateVideoDevices(stream) {
console.log('07. Get Video Devices');
navigator.mediaDevices
.enumerateDevices()
.then((devices) =>
devices.forEach((device) => {
let el,
eli = null;
if ('videoinput' === device.kind) {
el = videoSelect;
eli = initVideoSelect;
}
if (!el) return;
addChild(device, [el, eli]);
}),
)
.then(() => {
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
*/
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 local video inputs');
const videoConstraints = useVideo ? await getVideoConstraints('default') : false;
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
if (stream) {
localVideoMediaStream = stream;
await loadLocalMedia(stream, 'video');
console.log('10. Access granted to video device');
}
} catch (err) {
handleMediaError('video', err);
}
}
/**
* 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 local audio inputs');
const audioConstraints = useAudio ? await getAudioConstraints() : false;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraints });
if (stream) {
await loadLocalMedia(stream, 'audio');
if (useAudio) {
localAudioMediaStream = stream;
await getMicrophoneVolumeIndicator(stream);
console.log('10. Access granted to audio device');
}
}
} catch (err) {
handleMediaError('audio', err);
}
}
/**
* Handle media access error.
* @param {string} mediaType - 'video' or 'audio'
* @param {Error} err - The error object
*/
function handleMediaError(mediaType, err) {
console.error(`[Error] - Access denied for ${mediaType} device`, err);
}
/**
* 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 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';
// 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;
// my audio status element
myAudioStatusIcon.setAttribute('id', 'myAudioStatusIcon');
myAudioStatusIcon.className = className.audioOn;
// 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;
// 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(myVideoZoomInBtn, 'Zoom in video', 'bottom');
setTippy(myVideoPiPBtn, 'Toggle picture in picture', 'bottom');
setTippy(myVideoZoomOutBtn, 'Zoom out video', 'bottom');
setTippy(myVideoPinBtn, 'Toggle Pin video', '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);
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;
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();
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);
buttons.local.showVideoPipBtn && handlePictureInPicture(myVideoPiPBtn.id, myLocalMedia.id);
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_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 remotePeerKickOut = document.createElement('button');
const remoteVideoToImgBtn = document.createElement('button');
const remoteVideoFullScreenBtn = document.createElement('button');
const remoteVideoPinBtn = 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');
// 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;
// remote audio status element
remoteAudioStatusIcon.setAttribute('id', peer_id + '_audioStatus');
remoteAudioStatusIcon.className = className.audioOn;
// 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 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;
// 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(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');
}
// 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';
// attach to remote video nav bar
!isMobileDevice && remoteVideoNavBar.appendChild(remoteVideoPinBtn);
buttons.remote.showVideoPipBtn && remoteVideoNavBar.appendChild(remoteVideoPiPBtn);
if (buttons.remote.showZoomInOutBtn) {
remoteVideoNavBar.appendChild(remoteVideoZoomInBtn);
remoteVideoNavBar.appendChild(remoteVideoZoomOutBtn);
}
isVideoFullScreenSupported && remoteVideoNavBar.appendChild(remoteVideoFullScreenBtn);
buttons.remote.showSnapShotBtn && remoteVideoNavBar.appendChild(remoteVideoToImgBtn);
remoteVideoNavBar.appendChild(remoteVideoStatusIcon);
remoteVideoNavBar.appendChild(remoteAudioStatusIcon);
if (peer_audio && buttons.remote.showAudioVolume) {
remoteVideoNavBar.appendChild(remoteAudioVolume);
}
remoteVideoNavBar.appendChild(remoteHandStatusIcon);
buttons.remote.showPrivateMessageBtn && remoteVideoNavBar.appendChild(remotePrivateMsgBtn);
buttons.remote.showFileShareBtn && remoteVideoNavBar.appendChild(remoteFileShareBtn);
buttons.remote.showShareVideoAudioBtn && remoteVideoNavBar.appendChild(remoteVideoAudioUrlBtn);
buttons.remote.showKickOutBtn && remoteVideoNavBar.appendChild(remotePeerKickOut);
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;
remoteVideoWrap.className = 'Camera';
remoteVideoWrap.setAttribute('id', peer_id + '_videoWrap');
// 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 vide picture in picture
buttons.remote.showVideoPipBtn && handlePictureInPicture(remoteVideoPiPBtn.id, remoteMedia.id);
// handle video zoomIn/Out
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);
// 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 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, '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);
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);
}
/**
* 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
*/
function setPeerAvatarImgName(videoAvatarImageId, peerName) {
const videoAvatarImageElement = getId(videoAvatarImageId);
if (useAvatarSvg) {
const avatarImgSize = isMobileDevice ? 128 : 256;
const avatarImgSvg = genAvatarSvg(peerName, avatarImgSize);
videoAvatarImageElement.setAttribute('src', avatarImgSvg);
} 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
*/
function setPeerChatAvatarImgName(avatar, peerName) {
const avatarImg = 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;
}
}
/**
* 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();
});
videoPeer.addEventListener('drop', function (e) {
e.preventDefault();
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;
}
resizeVideoMedia();
}
/**
* 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
*/
function handlePictureInPicture(btnId, videoId) {
const btnPiP = getId(btnId);
const video = getId(videoId);
btnPiP.addEventListener('click', () => {
if (video.pictureInPictureElement) {
video.exitPictureInPicture();
} else if (document.pictureInPictureEnabled) {
video.requestPictureInPicture().catch((error) => {
console.error('Failed to enter Picture-in-Picture 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);
videoMediaContainer.style.top = 0;
videoMediaContainer.style.right = null;
videoMediaContainer.style.width = '100%';
videoMediaContainer.style.height = '100%';
pinnedVideoPlayerId = null;
isVideoPinned = false;
resizeVideoMedia();
}
}
/**
* 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 videoBtn = getId(videoToImgBtn);
const video = getId(videoStream);
videoBtn.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 manageLeftButtons() {
setShareRoomBtn();
setHideMeButton();
setAudioBtn();
setVideoBtn();
setSwapCameraBtn();
setScreenShareBtn();
setRecordStreamBtn();
setFullScreenBtn();
setChatRoomBtn();
setCaptionRoomBtn();
setRoomEmojiButton();
setChatEmojiBtn();
setMyHandBtn();
setMyWhiteboardBtn();
setMyFileShareBtn();
setDocumentPiPBtn();
setMySettingsBtn();
setAboutBtn();
setLeaveRoomBtn();
}
/**
* Copy - share room url button click event
*/
function setShareRoomBtn() {
shareRoomBtn.addEventListener('click', async (e) => {
shareRoomUrl();
});
}
/**
* Hide myself from room view
*/
function setHideMeButton() {
hideMeBtn.addEventListener('click', (e) => {
isHideMeActive = !isHideMeActive;
handleHideMe(isHideMeActive);
});
}
/**
* 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() {
if (browserName != 'Safari') {
// 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();
// open hide chat room
chatRoomBtn.addEventListener('click', (e) => {
if (!isChatRoomVisible) {
showChatRoomDraggable();
} else {
hideChatRoomAndEmojiPicker();
e.target.className = className.chatOn;
}
});
// 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();
}
});
// 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 () {
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 (speechRecognition && 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();
});
// 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);
// start recognition speech
speechRecognitionStart.addEventListener('click', (e) => {
startSpeech();
});
// stop recognition speech
speechRecognitionStop.addEventListener('click', (e) => {
stopSpeech();
});
} 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.native);
const message = {
type: 'roomEmoji',
room_id: roomId,
peer_name: myPeerName,
emoji: data.native,
};
if (thereArePeerConnections()) {
sendToServer('message', message);
}
handleEmoji(message);
}
function toggleEmojiPicker() {
if (emojiPickerContainer.style.display === 'block') {
elemDisplay(emojiPickerContainer, false);
setColor(roomEmojiPickerBtn, 'black');
} else {
emojiPickerContainer.style.display = 'block';
setColor(roomEmojiPickerBtn, 'green');
}
}
}
/**
* 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'));
});
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('change', (e) => {
wbIsLock = !wbIsLock;
whiteboardAction(getWhiteboardAction(wbIsLock ? 'lock' : 'unlock'));
});
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();
});
}
/**
* 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();
});
receiveHideBtn.addEventListener('click', (e) => {
hideFileTransfer();
});
}
/**
* 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();
});
}
/**
* 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;
let videoPIPAllowed = false;
// get video element
const videoPlayer = getId(video.id);
// Check if video can be add on pipVideo
if (video.id === 'myVideo') {
const localVideoStatus = getId('myVideoStatusIcon');
videoPIPAllowed =
localVideoStatus.className === className.videoOn && // video is ON
!videoPlayer.classList.contains('videoCircle'); // not in privacy mode
console.log('DOCUMENT PIP LOCAL videoPIPAllowed -----> ' + videoPIPAllowed);
} else {
const parts = video.id.split('___'); // peerId___video
const peer_id = parts[0];
const remoteVideoStatus = getId(peer_id + '_videoStatus');
videoPIPAllowed =
remoteVideoStatus.className === className.videoOn && // video is ON
!videoPlayer.classList.contains('videoCircle'); // not in privacy mode
console.log('DOCUMENT PIP REMOTE videoPIPAllowed -----> ' + videoPIPAllowed);
}
if (!videoPIPAllowed) return;
// Video is ON 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);
});
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);
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');
});
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 pause/resume
pauseRecBtn.addEventListener('click', (e) => {
pauseRecording();
});
resumeRecBtn.addEventListener('click', (e) => {
resumeRecording();
});
}
/**
* 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;
});
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');
});
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);
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value);
});
// advance audio options
micOptionsBtn.addEventListener('click', function () {
micDropDownMenu.style.display === 'block'
? elemDisplay(micDropDownMenu, false)
: elemDisplay(micDropDownMenu, true, 'block');
});
// audio options
switchAutoGainControl.onchange = (e) => {
lsSettings.mic_auto_gain_control = e.currentTarget.checked;
lS.setSettings(lsSettings);
e.target.blur();
};
switchEchoCancellation.onchange = (e) => {
lsSettings.mic_echo_cancellations = e.currentTarget.checked;
lS.setSettings(lsSettings);
e.target.blur();
};
switchNoiseSuppression.onchange = (e) => {
lsSettings.mic_noise_suppression = e.currentTarget.checked;
lS.setSettings(lsSettings);
e.target.blur();
};
sampleRateSelect.onchange = (e) => {
lsSettings.mic_sample_rate = e.currentTarget.selectedIndex;
lS.setSettings(lsSettings);
e.target.blur();
};
sampleSizeSelect.onchange = (e) => {
lsSettings.mic_sample_size = e.currentTarget.selectedIndex;
lS.setSettings(lsSettings);
e.target.blur();
};
channelCountSelect.onchange = (e) => {
lsSettings.mic_channel_count = e.currentTarget.selectedIndex;
lS.setSettings(lsSettings);
e.target.blur();
};
micLatencyRange.oninput = (e) => {
lsSettings.mic_latency = e.currentTarget.value;
lS.setSettings(lsSettings);
micLatencyValue.innerText = e.currentTarget.value;
e.target.blur();
};
micVolumeRange.oninput = (e) => {
lsSettings.mic_volume = e.currentTarget.value;
lS.setSettings(lsSettings);
micVolumeValue.innerText = e.currentTarget.value;
e.target.blur();
};
// apply audio options constraints
applyAudioOptionsBtn.addEventListener('click', async () => {
await changeLocalMicrophone(audioInputSelect.value);
micOptionsBtn.click();
});
// select audio output
audioOutputSelect.addEventListener('change', (e) => {
changeAudioDestination();
lS.setLocalStorageDevices(lS.MEDIA_TYPE.speaker, audioOutputSelect.selectedIndex, audioOutputSelect.value);
});
// select video input
videoSelect.addEventListener('change', async () => {
await changeLocalCamera(videoSelect.value);
await handleLocalCameraMirror();
await documentPictureInPictureClose();
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
});
// select video quality
videoQualitySelect.addEventListener('change', async (e) => {
await setLocalVideoQuality();
});
// 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);
});
}
// 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
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);
});
}
/**
* 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;
isAudioPitchBar = lsSettings.pitch_bar;
switchSounds.checked = notifyBySound;
switchShare.checked = notify;
switchAudioPitchBar.checked = isAudioPitchBar;
switchAutoGainControl.checked = lsSettings.mic_auto_gain_control;
switchEchoCancellation.checked = lsSettings.mic_echo_cancellations;
switchNoiseSuppression.checked = lsSettings.mic_noise_suppression;
sampleRateSelect.selectedIndex = lsSettings.mic_sample_rate;
sampleSizeSelect.selectedIndex = lsSettings.mic_sample_size;
channelCountSelect.selectedIndex = lsSettings.mic_channel_count;
micLatencyRange.value = lsSettings.mic_latency || '50';
micLatencyValue.innerText = lsSettings.mic_latency || '50';
micVolumeRange.value = lsSettings.mic_volume || '100';
micVolumeValue.innerText = lsSettings.mic_volume || '100';
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);
}
/**
* 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 (isDesktopDevice) {
// Desktop devices...
if (!initVideo.classList.contains('mirror')) {
initVideo.classList.toggle('mirror');
}
if (!myVideo.classList.contains('mirror')) {
myVideo.classList.toggle('mirror');
}
} else {
// Mobile, Tablet, IPad devices...
if (initVideo.classList.contains('mirror')) {
initVideo.classList.remove('mirror');
}
if (myVideo.classList.contains('mirror')) {
myVideo.classList.remove('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 = useAudio;
if (audioConstraints) {
audioConstraints = await getAudioConstraints();
audioConstraints['deviceId'] = audioSource ? { exact: audioSource } : undefined;
}
return {
audio: 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;
switch (videoQuality) {
case 'default':
if (forceCamMaxResolutionAndFps) {
// This will make the browser use the maximum resolution available as default, `up to 4K and 60fps`.
return {
width: { ideal: 3840 },
height: { ideal: 2160 },
frameRate: { ideal: 60 },
}; // video cam constraints default
}
// This will make the browser use hdVideo and 30fps.
return {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
}; // on default as hdVideo
case 'qvgaVideo':
return {
width: { exact: 320 },
height: { exact: 240 },
frameRate: frameRate,
}; // video cam constraints low bandwidth
case 'vgaVideo':
return {
width: { exact: 640 },
height: { exact: 480 },
frameRate: frameRate,
}; // video cam constraints medium bandwidth
case 'hdVideo':
return {
width: { exact: 1280 },
height: { exact: 720 },
frameRate: frameRate,
}; // video cam constraints high bandwidth
case 'fhdVideo':
return {
width: { exact: 1920 },
height: { exact: 1080 },
frameRate: frameRate,
}; // video cam constraints very high bandwidth
case '2kVideo':
return {
width: { exact: 2560 },
height: { exact: 1440 },
frameRate: frameRate,
}; // video cam constraints ultra high bandwidth
case '4kVideo':
return {
width: { exact: 3840 },
height: { exact: 2160 },
frameRate: frameRate,
}; // video cam constraints ultra high bandwidth
}
}
/**
* Get audio constraints
*/
async function getAudioConstraints() {
let constraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
},
video: false,
};
if (isRulesActive && isPresenter) {
constraints = {
audio: {
autoGainControl: switchAutoGainControl.checked,
echoCancellation: switchNoiseSuppression.checked,
noiseSuppression: switchEchoCancellation.checked,
sampleRate: parseInt(sampleRateSelect.value),
sampleSize: parseInt(sampleSizeSelect.value),
channelCount: parseInt(channelCountSelect.value),
latency: parseInt(micLatencyRange.value),
volume: parseInt(micVolumeRange.value / 100),
},
video: false,
};
}
return constraints;
// return {
// echoCancellation: true,
// noiseSuppression: true,
// };
}
/**
* 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) 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 Speaker
*/
function changeAudioDestination() {
const audioDestination = audioOutputSelect.value;
attachSinkId(myAudio, 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
*/
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 = `You need to use HTTPS for selecting audio output device: ${err}`;
console.error(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');
isButtonsVisible = true;
}
/**
* Check every 10 sec if need to hide buttons bar and status menu
*/
function checkButtonsBarAndMenu() {
if (!isButtonsBarOver) {
toggleClassElements('navbar', 'none');
elemDisplay(buttonsBar, 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() {
let qr = new QRious({
element: getId('qrRoom'),
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 selectedDateTime = document.getElementById('datetimePicker').value;
const newLine = '%0D%0A%0D%0A';
const email = '';
const emailSubject = `Please join our MiroTalk P2P Video Chat Meeting`;
const emailBody = `The meeting is scheduled at: ${newLine} DateTime: ${selectedDateTime} ${newLine} 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 isHostProtected && isPeerAuthEnabled
? window.location.origin + '/join/?room=' + roomId + '&username=' + myUsername + '&password=' + myPassword
: 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', 'top');
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);
}
if (!videoStatus) {
if (!isScreenStreaming) {
// Stop the video track based on the condition
if (init) {
await stopVideoTracks(initStream); // Stop init video track (camera LED off)
} else {
await stopVideoTracks(localVideoMediaStream); // Stop local video track (camera LED off)
await documentPictureInPictureClose(); // Close doc PIP if open
}
}
} else {
if (init) {
// Resume the video track for the init camera (camera LED on)
await changeInitCamera(initVideoSelect.value);
} else if (!isScreenStreaming) {
// Resume the video track for the local camera (camera LED on)
await changeLocalCamera(videoSelect.value);
}
}
setMyVideoStatus(videoStatus);
}
/**
* 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);
const constraints = {
audio: false,
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);
await refreshMyStreamToPeers(screenMediaPromise);
if (init) {
// Handle init media stream
if (initStream) 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 || 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);
// Determine the audio track to replace to peers
const myAudioTrack =
streamHasAudioTrack && (localAudioTrackChange || isScreenStreaming)
? stream.getAudioTracks()[0]
: localAudioMediaStream && localAudioMediaStream.getAudioTracks()[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(stream.getVideoTracks()[0]);
console.log('REPLACE VIDEO TRACK TO', { peer_id, peer_name });
} else {
// Add video track if sender does not exist
stream.getTracks().forEach((track) => {
if (track.kind === 'video') {
peerConnections[peer_id].addTrack(track);
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 });
}
});
}
// Replace audio track
const audioSender = peerConnections[peer_id].getSenders().find((s) => s.track && s.track.kind === 'audio');
if (audioSender) {
audioSender.replaceTrack(myAudioTrack);
console.log('REPLACE AUDIO TRACK TO', { peer_id, peer_name });
}
}
}
/**
* 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, 6000);
}
/**
* 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/webm;codecs=h264,opus',
'video/mp4;codecs=h264,aac',
'video/mp4',
];
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] };
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',
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} action recording action
*/
function notifyRecording(fromId, from, action) {
const msg = '🔴 ' + action + ' recording.';
const chatMessage = {
from: from,
fromId: fromId,
to: myPeerName,
msg: msg,
privateMsg: false,
};
handleDataChannelChat(chatMessage);
if (!showChatOnMessage) {
msgHTML(null, images.recording, null, `${from}<br /><h1>${action} recording</h1>`);
}
}
/**
* 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) {
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) {
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', '#000');
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>Size: ${blobFileSize}</li>
</ul>
<br/>
`;
lastRecordingInfo.innerHTML = `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);
isButtonsVisible = false;
}
chatRoomBtn.className = className.chatOff;
msgerDraggable.style.top = '50%';
msgerDraggable.style.left = isMobileDevice ? '50%' : '25%';
msgerDraggable.style.display = 'flex';
isChatRoomVisible = true;
setTippy(chatRoomBtn, 'Close the chat', placement);
}
/**
* Show caption box draggable on center screen position
*/
function showCaptionDraggable() {
playSound('newMessage');
if (isMobileDevice) {
elemDisplay(buttonsBar, false);
isButtonsVisible = false;
}
captionBtn.className = 'far fa-closed-captioning';
captionDraggable.style.top = '50%';
captionDraggable.style.left = isMobileDevice ? '50%' : '75%';
captionDraggable.style.display = 'flex';
isCaptionBoxVisible = true;
setTippy(captionBtn, 'Close the caption', placement);
}
/**
* 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();
setSP('--msger-width', '420px');
setSP('--msger-height', '680px');
}
/**
* Set chat position
*/
function chatCenter() {
msgerDraggable.style.top = '50%';
msgerDraggable.style.left = '50%';
}
/**
* 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();
setSP('--caption-width', '420px');
setSP('--caption-height', '680px');
}
/**
* Set caption position
*/
function captionCenter() {
captionDraggable.style.top = '50%';
captionDraggable.style.left = '50%';
}
/**
* Clean chat messages
*/
function cleanMessages() {
playSound('newMessage');
Swal.fire({
background: swBg,
position: 'center',
title: '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 object
chatMessages = [];
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() {
elemDisplay(msgerDraggable, false);
elemDisplay(msgerEmojiPicker, false);
setColor(msgerEmojiBtn, '#FFFFFF');
chatRoomBtn.className = className.chatOn;
isChatRoomVisible = false;
isChatEmojiVisible = false;
setTippy(chatRoomBtn, 'Open the chat', placement);
}
/**
* Hide chat room and emoji picker
*/
function hideCaptionBox() {
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, '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 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);
appendMessage(msgFrom, leftChatAvatar, 'left', msg, msgPrivate, msgId);
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);
const { peer_name, text_data } = config;
const time_stamp = getFormatDate(new Date());
const avatar_image = 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
*/
function appendMessage(from, img, side, msg, privateMsg, msgId = null) {
let time = getFormatDate(new Date());
// sanitize all params
const getFrom = filterXSS(from);
const getImg = filterXSS(img);
const getSide = filterXSS(side);
const getMsg = filterXSS(msg);
const getPrivateMsg = filterXSS(privateMsg);
const getMsgId = filterXSS(msgId);
// collect chat msges 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 id="${chatMessagesId}" class="msg-text">${getMsg}
<hr/>
`;
// add btn direct reply to private message
if (isValidPrivateMessage) {
msgHTML += `
<button
class="${className.msgPrivate}"
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('${chatMessagesId}')"
></button>`;
if (isSpeechSynthesisSupported) {
msgHTML += `
<button
id="msg-speech-${chatMessagesId}"
class="${className.speech}"
style="color:#fff; border:none; background:transparent;"
onclick="speechMessage(false, '${getFrom}', '${checkMsg(getMsg)}')"
></button>`;
}
msgHTML += `
</div>
</div>
</div>
`;
msgerChat.insertAdjacentHTML('beforeend', msgHTML);
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', 'top');
}
}
chatMessagesId++;
}
/**
* 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);
}
/**
* Delete message
* @param {string} id msg id
*/
function deleteMessage(id) {
playSound('newMessage');
Swal.fire({
background: swBg,
position: 'center',
title: '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'];
// 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 avatarSvg = 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" src="${avatarSvg}">
<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, toPeerName, pMsg, true, peerId);
appendMessage(myPeerName, rightChatAvatar, 'right', pMsg + '<hr>Private message to ' + toPeerName, true);
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(url) {
const pattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$',
'i', // fragment locator
);
return pattern.test(url);
}
/**
* Check if url passed is a image
* @param {string} url to check
* @returns {boolean} true/false
*/
function isImageURL(url) {
return url.match(/\.(jpeg|jpg|gif|png|tiff|bmp)$/) != null;
}
/**
* 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} 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, to, msg, privateMsg, id) {
if (!msg) return;
// sanitize all params
const getFrom = filterXSS(from);
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,
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,
},
})
.then(
function (completion) {
if (!completion) return;
setPeerChatAvatarImgName('left', 'ChatGPT');
appendMessage('ChatGPT', leftChatAvatar, 'left', completion, true);
cleanMessageInput();
speechInMessages ? speechMessage(true, 'ChatGPT', completion) : 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', placement);
isMySettingsVisible = true;
return;
}
elemDisplay(mySettings, false);
setTippy(mySettingsBtn, 'Open the settings', placement);
isMySettingsVisible = false;
}
/**
* 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,
});
myPeerNameSet.value = '';
myPeerNameSet.placeholder = myPeerName;
window.localStorage.peer_name = myPeerName;
setPeerAvatarImgName('myVideoAvatarImage', myPeerName);
setPeerAvatarImgName('myProfileAvatar', myPeerName);
setPeerChatAvatarImgName('right', myPeerName);
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 } = 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 = genAvatarSvg(peer_name, 32);
// refresh also peer video avatar name
setPeerAvatarImgName(peer_id + '_avatar', peer_name);
}
/**
* 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, 'black');
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', placement);
playSound('raiseHand');
} else {
// Lower hand
setColor(myHandBtn, 'black');
elemDisplay(myHandStatusIcon, false);
setTippy(myHandBtn, 'Lower your hand', placement);
}
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', placement);
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', placement);
}
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;
}
}
/**
* 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 Icon and Title
* @param {string} peer_id socket.id
* @param {boolean} status of peer audio
*/
function setPeerAudioStatus(peer_id, status) {
const peerAudioStatus = getId(peer_id + '_audioStatus');
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');
}
}
/**
* 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) disablePeer(peer_id, 'audio');
};
}
/**
* 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) disablePeer(peer_id, 'video');
};
}
/**
* 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, toPeerName, pMsg, true, toPeerId);
appendMessage(
myPeerName,
rightChatAvatar,
'right',
pMsg + '<br/><hr>Private message to ' + toPeerName,
true,
);
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_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_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_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, 'Start');
break;
case 'recStop':
notifyRecording(peer_id, peer_name, 'Stop');
break;
case 'screenStart':
handleScreenStart(peer_id);
break;
case 'screenStop':
handleScreenStop(peer_id, peer_use_video);
break;
case 'ejectAll':
handleKickedOut(config);
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 = '3vh';
emojiDisplay.style.color = '#FFF';
emojiDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
emojiDisplay.style.borderRadius = '10px';
emojiDisplay.innerText = `${message.emoji} ${message.peer_name}`;
userEmoji.appendChild(emojiDisplay);
setTimeout(() => {
emojiDisplay.remove();
}, duration);
}
}
/**
* 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;
}
}
});
}
/**
* 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;
}
}
});
}
/**
* 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;
}
} 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;
}
}
/**
* 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();
}
/**
* 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':
Swal.fire({
allowOutsideClick: false,
background: swBg,
position: 'center',
title: 'Select image',
input: 'file',
inputAttributes: {
accept: wbImageInput,
'aria-label': 'Select image',
},
showDenyButton: true,
confirmButtonText: `OK`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
let wbCanvasImg = result.value;
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);
} else {
userLog('error', 'File not selected or empty');
}
}
});
break;
case 'pdfFile':
Swal.fire({
allowOutsideClick: false,
background: swBg,
position: 'center',
title: 'Select the PDF',
input: 'file',
inputAttributes: {
accept: wbPdfInput,
'aria-label': 'Select the PDF',
},
showDenyButton: true,
confirmButtonText: `OK`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
let wbCanvasPdf = result.value;
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);
} else {
userLog('error', 'File not selected or empty', 'top-end');
}
}
});
break;
case 'text':
const text = new fabric.IText('Lorem Ipsum', {
top: 0,
left: 0,
fontFamily: 'Comfortaa',
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;
}
}
/**
* 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;
//...
}
}
/**
* 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,
});
}
}
/**
* 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 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',
inputAttributes: {
accept: fileSharingInput,
'aria-label': 'Select file',
},
showDenyButton: true,
confirmButtonText: `Send`,
denyButtonText: `Cancel`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
sendFileInformations(result.value, 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_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);
// 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);
}
});
}
/**
* 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;
}
}
/**
* 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) => {
kickOut(peer_id);
});
}
/**
* 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,
});
}
});
}
/**
* 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: '<strong>WebRTC P2P</strong>',
imageAlt: 'mirotalk-about',
imageUrl: images.about,
customClass: { image: 'img-about' },
html: `
<br/>
<div id="about">
<button
id="support-button"
data-umami-event="Support button"
class="pulsate"
onclick="window.open('https://codecanyon.net/user/miroslavpejic85')">
<i class="${className.heart}" ></i>&nbsp;Support
</button>
<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>
</div>
`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
/**
* Leave the Room and create a new one
*/
function leaveRoom() {
checkRecording();
if (surveyActive) {
leaveFeedback();
} else {
redirectOnLeave();
}
}
/**
* Ask for feedback when room exit
*/
function leaveFeedback() {
Swal.fire({
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: true,
background: swBg,
imageUrl: images.feedback,
title: 'Leave a feedback',
text: 'Do you want to rate your MiroTalk experience?',
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
}).then((result) => {
if (result.isConfirmed) {
openURL(surveyURL);
} else {
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;
}
}
/**
* 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) {
if (!isAudioPitchBar) return;
const peer_id = data.peer_id;
const remotePitchBar = getId(peer_id + '_pitch_bar');
//let remoteVideoWrap = getId(peer_id + '_videoWrap');
if (!remotePitchBar) return;
const volume = data.volume;
if (volume > 50) {
remotePitchBar.style.backgroundColor = 'orange';
}
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) {
if (!isAudioPitchBar || !myPitchBar) return;
const volume = data.volume;
if (volume > 50) {
myPitchBar.style.backgroundColor = 'orange';
}
myPitchBar.style.height = volume + '%';
//myVideoWrap.classList.toggle('speaking');
setTimeout(function () {
myPitchBar.style.backgroundColor = '#19bb5c';
myPitchBar.style.height = '0%';
//myVideoWrap.classList.toggle('speaking');
}, 100);
}
/**
* 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__rubberBand' },
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',
title: message,
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
break;
// ......
default:
alert(message);
}
}
/**
* 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
*/
function msgHTML(icon, imageUrl, title, html, position = 'center') {
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' },
});
}
/**
* 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) {
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
*/
async function playSound(name, force = false) {
if (!notifyBySound && !force) return;
const sound = '../sounds/' + 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 Tablet device
* @param {object} userAgent info
* @return {boolean} true/false
*/
function isTablet(userAgent) {
return /(ipad|tablet|(android(?!.*mobile))|(windows(?!.*phone)(.*touch))|kindle|playbook|silk|(puffin(?!.*(IP|AP|WP))))/.test(
userAgent,
);
}
/**
* Check if IPad device
* @param {object} userAgent
* @return {boolean} true/false
*/
function isIpad(userAgent) {
return /macintosh/.test(userAgent) && 'ontouchend' in document;
}
/**
* Get Html element by Id
* @param {string} id of the element
* @returns {object} element
*/
function getId(id) {
return document.getElementById(id);
}
/**
* 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;
}