'use strict'; const cfg = { msgAvatar: 'https://eu.ui-avatars.com/api', }; const html = { newline: '
', audioOn: 'fas fa-microphone', audioOff: 'fas fa-microphone-slash', videoOn: 'fas fa-video', videoOff: 'fas fa-video-slash', userName: 'fas fa-user username', userHand: 'fas fa-hand-paper pulsate', fullScreen: 'fas fa-expand', snapshot: 'fas fa-camera-retro', }; const image = { poster: '../images/loader.gif', delete: '../images/delete.png', locked: '../images/locked.png', mute: '../images/mute.png', hide: '../images/hide.png', users: '../images/participants.png', user: '../images/participant.png', youtube: '../images/youtube.png', message: '../images/message.png', share: '../images/share.png', exit: '../images/exit.png', }; const mediaType = { audio: 'audioType', video: 'videoType', camera: 'cameraType', screen: 'screenType', }; const _EVENTS = { openRoom: 'openRoom', exitRoom: 'exitRoom', startRec: 'startRec', pauseRec: 'pauseRec', resumeRec: 'resumeRec', stopRec: 'stopRec', raiseHand: 'raiseHand', lowerHand: 'lowerHand', startVideo: 'startVideo', pauseVideo: 'pauseVideo', resumeVideo: 'resumeVideo', stopVideo: 'stopVideo', startAudio: 'startAudio', pauseAudio: 'pauseAudio', resumeAudio: 'resumeAudio', stopAudio: 'stopAudio', startScreen: 'startScreen', pauseScreen: 'pauseScreen', resumeScreen: 'resumeScreen', stopScreen: 'stopScreen', roomLock: 'roomLock', roomUnlock: 'roomUnlock', }; let recordedBlobs; class RoomClient { constructor( remoteAudioEl, videoMediaContainer, mediasoupClient, socket, room_id, peer_name, peer_geo, peer_info, isAudioAllowed, isVideoAllowed, successCallback, ) { this.remoteAudioEl = remoteAudioEl; this.videoMediaContainer = videoMediaContainer; this.mediasoupClient = mediasoupClient; this.socket = socket; this.room_id = room_id; this.peer_id = socket.id; this.peer_name = peer_name; this.peer_geo = peer_geo; this.peer_info = peer_info; this.isAudioAllowed = isAudioAllowed; this.isVideoAllowed = isVideoAllowed; this.producerTransport = null; this.consumerTransport = null; this.device = null; this.isMobileDevice = DetectRTC.isMobileDevice; this.isMySettingsOpen = false; this._isConnected = false; this.isVideoOnFullScreen = false; this.isChatOpen = false; this.isChatEmojiOpen = false; this.camVideo = false; this.camera = 'user'; this.chatMessages = []; this.leftMsgAvatar = null; this.rightMsgAvatar = null; this.localVideoStream = null; this.localScreenStream = null; this.localAudioStream = null; this.mediaRecorder = null; this.recScreenStream = null; this._isRecording = false; this.RoomPassword = null; // file transfer settings this.fileToSend = null; this.fileReader = null; this.receiveBuffer = []; this.receivedSize = 0; this.incomingFileInfo = null; this.incomingFileData = null; this.sendInProgress = false; this.receiveInProgress = false; this.fileSharingInput = '*'; this.chunkSize = 1024 * 16; // 16kb/s this.myVideoEl = null; this.debug = false; this.videoProducerId = null; this.audioProducerId = null; this.consumers = new Map(); this.producers = new Map(); this.producerLabel = new Map(); this.eventListeners = new Map(); console.log('06 ----> Load Mediasoup Client v', mediasoupClient.version); Object.keys(_EVENTS).forEach( function (evt) { this.eventListeners.set(evt, []); }.bind(this), ); this.socket.request = function request(type, data = {}) { return new Promise((resolve, reject) => { socket.emit(type, data, (data) => { if (data.error) { reject(data.error); } else { resolve(data); } }); }); }; // #################################################### // CREATE ROOM AND JOIN // #################################################### this.createRoom(this.room_id).then( async function () { let data = { room_id: this.room_id, peer_info: this.peer_info, peer_geo: this.peer_geo, }; await this.join(data); this.initSockets(); this._isConnected = true; successCallback(); }.bind(this), ); } // #################################################### // GET STARTED // #################################################### async createRoom(room_id) { await this.socket .request('createRoom', { room_id, }) .catch((err) => { console.log('Create room error:', err); }); } async join(data) { socket .request('join', data) .then( async function (room) { if (room === 'isLocked') { this.event(_EVENTS.roomLock); console.log('00-WARNING ----> Room is Locked, Try to unlock by the password'); this.unlockTheRoom(); return; } await this.joinAllowed(room); }.bind(this), ) .catch((err) => { console.log('Join error:', err); }); } async joinAllowed(room) { await this.handleRoomInfo(room); const data = await this.socket.request('getRouterRtpCapabilities'); this.device = await this.loadDevice(data); console.log('07 ----> Get Router Rtp Capabilities codecs: ', this.device.rtpCapabilities.codecs); await this.initTransports(this.device); this.startLocalMedia(); this.socket.emit('getProducers'); } async handleRoomInfo(room) { let peers = new Map(JSON.parse(room.peers)); participantsCount = peers.size; adaptAspectRatio(participantsCount); for (let peer of Array.from(peers.keys()).filter((id) => id !== this.peer_id)) { let peer_info = peers.get(peer).peer_info; // console.log('07 ----> Remote Peer info', peer_info); if (!peer_info.peer_video) { await this.setVideoOff(peer_info, true); } } this.refreshParticipantsCount(); } async loadDevice(routerRtpCapabilities) { let device; try { device = new this.mediasoupClient.Device(); } catch (error) { if (error.name === 'UnsupportedError') { console.error('Browser not supported'); this.userLog('error', 'Browser not supported', 'center'); } console.error('Browser not supported: ', error); this.userLog('error', 'Browser not supported: ' + error, 'center'); } await device.load({ routerRtpCapabilities, }); return device; } // #################################################### // PRODUCER TRANSPORT // #################################################### async initTransports(device) { { const data = await this.socket.request('createWebRtcTransport', { forceTcp: false, rtpCapabilities: device.rtpCapabilities, }); if (data.error) { console.error('Create WebRtc Transport for Producer err: ', data.error); return; } this.producerTransport = device.createSendTransport(data); this.producerTransport.on( 'connect', async function ({ dtlsParameters }, callback, errback) { this.socket .request('connectTransport', { dtlsParameters, transport_id: data.id, }) .then(callback) .catch(errback); }.bind(this), ); this.producerTransport.on( 'produce', async function ({ kind, rtpParameters }, callback, errback) { try { const { producer_id } = await this.socket.request('produce', { producerTransportId: this.producerTransport.id, kind, rtpParameters, }); callback({ id: producer_id, }); } catch (err) { errback(err); } }.bind(this), ); this.producerTransport.on( 'connectionstatechange', function (state) { switch (state) { case 'connecting': break; case 'connected': console.log('Producer Transport connected'); break; case 'failed': console.warn('Producer Transport failed'); this.producerTransport.close(); break; default: break; } }.bind(this), ); } // #################################################### // CONSUMER TRANSPORT // #################################################### { const data = await this.socket.request('createWebRtcTransport', { forceTcp: false, }); if (data.error) { console.error('Create WebRtc Transport for Consumer err: ', data.error); return; } this.consumerTransport = device.createRecvTransport(data); this.consumerTransport.on( 'connect', function ({ dtlsParameters }, callback, errback) { this.socket .request('connectTransport', { transport_id: this.consumerTransport.id, dtlsParameters, }) .then(callback) .catch(errback); }.bind(this), ); this.consumerTransport.on( 'connectionstatechange', async function (state) { switch (state) { case 'connecting': break; case 'connected': console.log('Consumer Transport connected'); break; case 'failed': console.warn('Consumer Transport failed'); this.consumerTransport.close(); break; default: break; } }.bind(this), ); } } // #################################################### // TODO DATACHANNEL TRANSPORT // #################################################### // #################################################### // SOCKET ON // #################################################### initSockets() { this.socket.on( 'consumerClosed', function ({ consumer_id }) { console.log('Closing consumer:', consumer_id); this.removeConsumer(consumer_id); }.bind(this), ); this.socket.on( 'setVideoOff', function (data) { console.log('Video off:', data); this.setVideoOff(data, true); }.bind(this), ); this.socket.on( 'removeMe', function (data) { console.log('Remove me:', data); this.removeVideoOff(data.peer_id); participantsCount = data.peer_counts; adaptAspectRatio(participantsCount); }.bind(this), ); this.socket.on( 'refreshParticipantsCount', function (data) { console.log('Participants Count:', data); participantsCount = data.peer_counts; adaptAspectRatio(participantsCount); }.bind(this), ); this.socket.on( 'newProducers', async function (data) { if (data.length > 0) { console.log('New producers', data); for (let { producer_id, peer_name, peer_info } of data) { await this.consume(producer_id, peer_name, peer_info); } } }.bind(this), ); this.socket.on( 'message', function (data) { console.log('New message:', data); this.showMessage(data); }.bind(this), ); this.socket.on( 'roomAction', function (data) { console.log('Room action:', data); this.roomAction(data, false); }.bind(this), ); this.socket.on( 'roomPassword', function (data) { console.log('Room password:', data.password); this.roomPassword(data); }.bind(this), ); this.socket.on( 'peerAction', function (data) { console.log('Peer action:', data); this.peerAction(data.from_peer_name, data.peer_id, data.action, false, data.broadcast); }.bind(this), ); this.socket.on( 'updatePeerInfo', function (data) { console.log('Peer info update:', data); this.updatePeerInfo(data.peer_name, data.peer_id, data.type, data.status, false); }.bind(this), ); this.socket.on( 'fileInfo', function (data) { console.log('File info:', data); this.handleFileInfo(data); }.bind(this), ); this.socket.on( 'file', function (data) { this.handleFile(data); }.bind(this), ); this.socket.on( 'youTubeAction', function (data) { this.youTubeAction(data); }.bind(this), ); this.socket.on( 'fileAbort', function (data) { this.handleFileAbort(data); }.bind(this), ); this.socket.on( 'wbCanvasToJson', function (data) { console.log('Received whiteboard canvas JSON'); JsonToWbCanvas(data); }.bind(this), ); this.socket.on( 'whiteboardAction', function (data) { console.log('Whiteboard action', data); whiteboardAction(data, false); }.bind(this), ); this.socket.on( 'audioVolume', function (data) { this.handleAudioVolume(data); }.bind(this), ); this.socket.on( 'disconnect', function () { this.exit(true); }.bind(this), ); } // #################################################### // START LOCAL AUDIO VIDEO MEDIA // #################################################### startLocalMedia() { if (this.isAudioAllowed) { console.log('08 ----> Start audio media'); this.produce(mediaType.audio, microphoneSelect.value); } else { setColor(startAudioButton, 'red'); } if (this.isVideoAllowed) { console.log('09 ----> Start video media'); this.produce(mediaType.video, videoSelect.value); } else { setColor(startVideoButton, 'red'); console.log('09 ----> Video is off'); this.setVideoOff(this.peer_info, false); this.sendVideoOff(); } } // #################################################### // PRODUCER // #################################################### async produce(type, deviceId = null, swapCamera = false) { let mediaConstraints = {}; let audio = false; let screen = false; switch (type) { case mediaType.audio: mediaConstraints = this.getAudioConstraints(deviceId); audio = true; break; case mediaType.video: if (swapCamera) { mediaConstraints = this.getCameraConstraints(); } else { mediaConstraints = this.getVideoConstraints(deviceId); } break; case mediaType.screen: mediaConstraints = this.getScreenConstraints(); screen = true; break; default: return; } if (!this.device.canProduce('video') && !audio) { console.error('Cannot produce video'); return; } if (this.producerLabel.has(type)) { console.log('Producer already exists for this type ' + type); return; } console.log(`Media contraints ${type}:`, mediaConstraints); let stream; try { stream = screen ? await navigator.mediaDevices.getDisplayMedia() : await navigator.mediaDevices.getUserMedia(mediaConstraints); console.log('Supported Constraints', navigator.mediaDevices.getSupportedConstraints()); const track = audio ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]; const params = { track, }; if (!audio && !screen) { params.encodings = this.getEncoding(); params.codecOptions = { videoGoogleStartBitrate: 1000, }; } producer = await this.producerTransport.produce(params); console.log('PRODUCER', producer); this.producers.set(producer.id, producer); let elem; if (!audio) { this.localVideoStream = stream; elem = await this.handleProducer(producer.id, type, stream); this.videoProducerId = producer.id; } else { this.localAudioStream = stream; this.audioProducerId = producer.id; } producer.on('trackended', () => { this.closeProducer(type); }); producer.on('transportclose', () => { console.log('Producer transport close'); if (!audio) { elem.srcObject.getTracks().forEach(function (track) { track.stop(); }); elem.parentNode.removeChild(elem); handleAspectRatio(); console.log('[transportClose] Video-element-count', this.videoMediaContainer.childElementCount); } this.producers.delete(producer.id); }); producer.on('close', () => { console.log('Closing producer'); if (!audio) { elem.srcObject.getTracks().forEach(function (track) { track.stop(); }); elem.parentNode.removeChild(elem); handleAspectRatio(); console.log('[closingProducer] Video-element-count', this.videoMediaContainer.childElementCount); } this.producers.delete(producer.id); }); this.producerLabel.set(type, producer.id); switch (type) { case mediaType.audio: this.setIsAudio(this.peer_id, true); this.event(_EVENTS.startAudio); break; case mediaType.video: this.setIsVideo(true); this.event(_EVENTS.startVideo); break; case mediaType.screen: this.setIsScreen(true); this.event(_EVENTS.startScreen); break; default: return; } } catch (err) { console.error('Produce error:', err); } } getAudioConstraints(deviceId) { return { audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 44100, deviceId: deviceId, }, video: false, }; } getCameraConstraints() { this.camera = this.camera == 'user' ? 'environment' : 'user'; if (this.camera != 'user') this.camVideo = { facingMode: { exact: this.camera } }; else this.camVideo = true; return { audio: false, video: this.camVideo, }; } getVideoConstraints(deviceId) { return { audio: false, video: { width: { min: 640, ideal: 1920, max: 3840, }, height: { min: 480, ideal: 1080, max: 2160, }, deviceId: deviceId, aspectRatio: 1.777, // 16:9 frameRate: { min: 5, ideal: 15, max: 30, }, }, }; } getScreenConstraints() { return { video: { frameRate: { min: 5, ideal: 15, max: 30, }, }, }; } getEncoding() { return [ { rid: 'r0', maxBitrate: 100000, scalabilityMode: 'S1T3', }, { rid: 'r1', maxBitrate: 300000, scalabilityMode: 'S1T3', }, { rid: 'r2', maxBitrate: 900000, scalabilityMode: 'S1T3', }, ]; } closeThenProduce(type, deviceId, swapCamera = false) { this.closeProducer(type); this.produce(type, deviceId, swapCamera); } async handleProducer(id, type, stream) { let elem, vb, ts, d, p, i, b, fs, pm, pb; this.removeVideoOff(this.peer_id); d = document.createElement('div'); d.className = 'Camera'; d.id = id + '__d'; elem = document.createElement('video'); elem.setAttribute('id', id); elem.setAttribute('playsinline', true); elem.autoplay = true; elem.poster = image.poster; this.isMobileDevice || type === mediaType.screen ? (elem.className = '') : (elem.className = 'mirror'); vb = document.createElement('div'); vb.setAttribute('id', this.peer_id + '__vb'); vb.className = 'videoMenuBar fadein'; fs = document.createElement('button'); fs.id = id + '__fullScreen'; fs.className = html.fullScreen; ts = document.createElement('button'); ts.id = id + '__snapshot'; ts.className = html.snapshot; b = document.createElement('button'); b.id = this.peer_id + '__audio'; b.className = this.peer_info.peer_audio ? html.audioOn : html.audioOff; p = document.createElement('p'); p.id = this.peer_id + '__name'; p.className = html.userName; p.innerHTML = ' ' + this.peer_name + ' (me)'; i = document.createElement('i'); i.id = this.peer_id + '__hand'; i.className = html.userHand; pm = document.createElement('div'); pb = document.createElement('div'); pm.setAttribute('id', this.peer_id + '_pitchMeter'); pb.setAttribute('id', this.peer_id + '_pitchBar'); pm.className = 'speechbar'; pb.className = 'bar'; pb.style.height = '1%'; pm.appendChild(pb); vb.appendChild(b); vb.appendChild(ts); vb.appendChild(fs); d.appendChild(elem); d.appendChild(pm); d.appendChild(i); d.appendChild(p); d.appendChild(vb); this.videoMediaContainer.appendChild(d); this.attachMediaStream(elem, stream, type, 'Producer'); this.myVideoEl = elem; this.handleFS(elem.id, fs.id); this.handleTS(elem.id, ts.id); this.popupPeerInfo(p.id, this.peer_info); this.checkPeerInfoStatus(this.peer_info); this.sound('joined'); handleAspectRatio(); console.log('[addProducer] Video-element-count', this.videoMediaContainer.childElementCount); if (!this.isMobileDevice) { this.setTippy(elem.id, 'Full Screen', 'top-end'); this.setTippy(ts.id, 'Snapshot', 'top-end'); } return elem; } pauseProducer(type) { if (!this.producerLabel.has(type)) { console.log('There is no producer for this type ' + type); return; } let producer_id = this.producerLabel.get(type); this.producers.get(producer_id).pause(); switch (type) { case mediaType.audio: this.event(_EVENTS.pauseAudio); break; case mediaType.video: this.event(_EVENTS.pauseVideo); break; case mediaType.screen: this.event(_EVENTS.pauseScreen); break; default: return; } } resumeProducer(type) { if (!this.producerLabel.has(type)) { console.log('There is no producer for this type ' + type); return; } let producer_id = this.producerLabel.get(type); this.producers.get(producer_id).resume(); switch (type) { case mediaType.audio: this.event(_EVENTS.resumeAudio); break; case mediaType.video: this.event(_EVENTS.resumeVideo); break; case mediaType.screen: this.event(_EVENTS.resumeScreen); break; default: return; } } closeProducer(type) { if (!this.producerLabel.has(type)) { console.log('There is no producer for this type ' + type); return; } let producer_id = this.producerLabel.get(type); let data = { peer_name: this.peer_name, producer_id: producer_id, type: type, status: false, }; console.log('Close producer', data); this.socket.emit('producerClosed', data); this.producers.get(producer_id).close(); this.producers.delete(producer_id); this.producerLabel.delete(type); if (type !== mediaType.audio) { let elem = this.getId(producer_id); let d = this.getId(producer_id + '__d'); elem.srcObject.getTracks().forEach(function (track) { track.stop(); }); d.parentNode.removeChild(d); handleAspectRatio(); console.log('[producerClose] Video-element-count', this.videoMediaContainer.childElementCount); } switch (type) { case mediaType.audio: this.setIsAudio(this.peer_id, false); this.event(_EVENTS.stopAudio); break; case mediaType.video: this.setIsVideo(false); this.event(_EVENTS.stopVideo); break; case mediaType.screen: this.setIsScreen(false); this.event(_EVENTS.stopScreen); break; default: return; } this.sound('left'); } // #################################################### // CONSUMER // #################################################### async consume(producer_id, peer_name, peer_info) { this.getConsumeStream(producer_id).then( function ({ consumer, stream, kind }) { console.log('CONSUMER', consumer); this.consumers.set(consumer.id, consumer); if (kind === 'video') { this.handleConsumer(consumer.id, mediaType.video, stream, peer_name, peer_info); } else { this.handleConsumer(consumer.id, mediaType.audio, stream, peer_name, peer_info); } consumer.on( 'trackended', function () { this.removeConsumer(consumer.id); }.bind(this), ); consumer.on( 'transportclose', function () { this.removeConsumer(consumer.id); }.bind(this), ); }.bind(this), ); } async getConsumeStream(producerId) { const { rtpCapabilities } = this.device; const data = await this.socket.request('consume', { rtpCapabilities, consumerTransportId: this.consumerTransport.id, producerId, }); const { id, kind, rtpParameters } = data; const codecOptions = {}; const consumer = await this.consumerTransport.consume({ id, producerId, kind, rtpParameters, codecOptions, }); const stream = new MediaStream(); stream.addTrack(consumer.track); return { consumer, stream, kind, }; } handleConsumer(id, type, stream, peer_name, peer_info) { let elem, vb, d, p, i, b, fs, ts, pb, pm; switch (type) { case mediaType.video: let remotePeerId = peer_info.peer_id; let remotePeerAudio = peer_info.peer_audio; this.removeVideoOff(remotePeerId); d = document.createElement('div'); d.className = 'Camera'; d.id = id + '__d'; elem = document.createElement('video'); elem.setAttribute('id', id); elem.setAttribute('playsinline', true); elem.autoplay = true; elem.className = ''; elem.poster = image.poster; vb = document.createElement('div'); vb.setAttribute('id', remotePeerId + '__vb'); vb.className = 'videoMenuBar fadein'; fs = document.createElement('button'); fs.id = id + '__fullScreen'; fs.className = html.fullScreen; ts = document.createElement('button'); ts.id = id + '__snapshot'; ts.className = html.snapshot; b = document.createElement('button'); b.id = remotePeerId + '__audio'; b.className = remotePeerAudio ? html.audioOn : html.audioOff; i = document.createElement('i'); i.id = remotePeerId + '__hand'; i.className = html.userHand; p = document.createElement('p'); p.id = remotePeerId + '__name'; p.className = html.userName; p.innerHTML = ' ' + peer_name; pm = document.createElement('div'); pb = document.createElement('div'); pm.setAttribute('id', remotePeerId + '__pitchMeter'); pb.setAttribute('id', remotePeerId + '__pitchBar'); pm.className = 'speechbar'; pb.className = 'bar'; pb.style.height = '1%'; pm.appendChild(pb); vb.appendChild(b); vb.appendChild(ts); vb.appendChild(fs); d.appendChild(elem); d.appendChild(i); d.appendChild(p); d.appendChild(pm); d.appendChild(vb); this.videoMediaContainer.appendChild(d); this.attachMediaStream(elem, stream, type, 'Consumer'); this.handleFS(elem.id, fs.id); this.handleTS(elem.id, ts.id); this.popupPeerInfo(p.id, peer_info); this.checkPeerInfoStatus(peer_info); this.sound('joined'); handleAspectRatio(); console.log('[addConsumer] Video-element-count', this.videoMediaContainer.childElementCount); if (!this.isMobileDevice) { this.setTippy(elem.id, 'Full Screen', 'top-end'); this.setTippy(ts.id, 'Snapshot', 'top-end'); } break; case mediaType.audio: elem = document.createElement('audio'); elem.id = id; elem.autoplay = true; this.remoteAudioEl.appendChild(elem); this.attachMediaStream(elem, stream, type, 'Consumer'); break; } return elem; } removeConsumer(consumer_id) { console.log('Remove consumer_id:', consumer_id); let elem = this.getId(consumer_id); let d = this.getId(consumer_id + '__d'); elem.srcObject.getTracks().forEach(function (track) { track.stop(); }); if (elem) elem.parentNode.removeChild(elem); if (d) d.parentNode.removeChild(d); handleAspectRatio(); console.log('[removeConsumer] Video-element-count', this.videoMediaContainer.childElementCount); this.consumers.delete(consumer_id); this.sound('left'); } // #################################################### // HANDLE VIDEO OFF // #################################################### async setVideoOff(peer_info, remotePeer = false) { let d, vb, i, h, b, p, pm, pb; let peer_id = peer_info.peer_id; let peer_name = peer_info.peer_name; let peer_audio = peer_info.peer_audio; this.removeVideoOff(peer_id); d = document.createElement('div'); d.className = 'Camera'; d.id = peer_id + '__videoOff'; vb = document.createElement('div'); vb.setAttribute('id', this.peer_id + 'vb'); vb.className = 'videoMenuBar fadein'; b = document.createElement('button'); b.id = peer_id + '__audio'; b.className = peer_audio ? html.audioOn : html.audioOff; i = document.createElement('img'); i.className = 'center pulsate'; i.id = peer_id + '__img'; p = document.createElement('p'); p.id = peer_id + '__name'; p.className = html.userName; p.innerHTML = ' ' + peer_name + (remotePeer ? '' : ' (me) '); h = document.createElement('i'); h.id = peer_id + '__hand'; h.className = html.userHand; pm = document.createElement('div'); pb = document.createElement('div'); pm.setAttribute('id', peer_id + '__pitchMeter'); pb.setAttribute('id', peer_id + '__pitchBar'); pm.className = 'speechbar'; pb.className = 'bar'; pb.style.height = '1%'; pm.appendChild(pb); vb.appendChild(b); d.appendChild(i); d.appendChild(p); d.appendChild(h); d.appendChild(pm); d.appendChild(vb); this.videoMediaContainer.appendChild(d); this.setVideoAvatarImgName(i.id, peer_name); this.getId(i.id).style.display = 'block'; handleAspectRatio(); console.log('[setVideoOff] Video-element-count', this.videoMediaContainer.childElementCount); this.sound('joined'); } removeVideoOff(peer_id) { let pvOff = this.getId(peer_id + '__videoOff'); if (pvOff) { pvOff.parentNode.removeChild(pvOff); handleAspectRatio(); console.log('[removeVideoOff] Video-element-count', this.videoMediaContainer.childElementCount); this.sound('left'); } } // #################################################### // EXIT ROOM // #################################################### exit(offline = false) { let clean = function () { this._isConnected = false; this.consumerTransport.close(); this.producerTransport.close(); this.socket.off('disconnect'); this.socket.off('newProducers'); this.socket.off('consumerClosed'); }.bind(this); if (!offline) { this.socket .request('exitRoom') .then((e) => console.log('Exit Room', e)) .catch((e) => console.warn('Exit Room ', e)) .finally( function () { clean(); }.bind(this), ); } else { clean(); } this.event(_EVENTS.exitRoom); } exitRoom() { this.sound('open'); Swal.fire({ background: swalBackground, imageAlt: 'mirotalksfu-leave-room', imageUrl: image.exit, position: 'center', title: 'Leave this room?', showDenyButton: true, confirmButtonText: `Yes`, denyButtonText: `No`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }).then((result) => { if (result.isConfirmed) this.exit(); }); } // #################################################### // HELPERS // #################################################### attachMediaStream(elem, stream, type, who) { let track; switch (type) { case mediaType.audio: track = stream.getAudioTracks()[0]; break; case mediaType.video: case mediaType.screen: track = stream.getVideoTracks()[0]; break; } const newStream = new MediaStream(); newStream.addTrack(track); elem.srcObject = newStream; console.log(who + ' Success attached media ' + type); } attachSinkId(elem, sinkId) { if (typeof elem.sinkId !== 'undefined') { elem.setSinkId(sinkId) .then(() => { console.log(`Success, audio output device attached: ${sinkId}`); }) .catch((err) => { let errorMessage = err; if (err.name === 'SecurityError') errorMessage = `You need to use HTTPS for selecting audio output device: ${err}`; console.error('Attach SinkId error: ', errorMessage); this.userLog('error', errorMessage, 'top-end'); this.getId('speakerSelect').selectedIndex = 0; }); } else { let error = `Browser seems doesn't support output device selection.`; console.warn(error); this.userLog('error', error, 'top-end'); } } event(evt) { if (this.eventListeners.has(evt)) { this.eventListeners.get(evt).forEach((callback) => callback()); } } on(evt, callback) { this.eventListeners.get(evt).push(callback); } // #################################################### // SET // #################################################### setTippy(elem, content, placement) { if (DetectRTC.isMobileDevice) return; tippy(this.getId(elem), { content: content, placement: placement, }); } setVideoAvatarImgName(elemId, peer_name) { let elem = this.getId(elemId); let avatarImgSize = 250; elem.setAttribute( 'src', cfg.msgAvatar + '?name=' + peer_name + '&size=' + avatarImgSize + '&background=random&rounded=true', ); } setIsAudio(peer_id, status) { this.peer_info.peer_audio = status; let b = this.getPeerAudioBtn(peer_id); if (b) b.className = this.peer_info.peer_audio ? html.audioOn : html.audioOff; } setIsVideo(status) { this.peer_info.peer_video = status; if (!this.peer_info.peer_video) { this.setVideoOff(this.peer_info, false); this.sendVideoOff(); } } setIsScreen(status) { this.peer_info.peer_screen = status; if (!this.peer_info.peer_screen && !this.peer_info.peer_video) { this.setVideoOff(this.peer_info, false); this.sendVideoOff(); } } sendVideoOff() { this.socket.emit('setVideoOff', this.peer_info); } // #################################################### // GET // #################################################### isConnected() { return this._isConnected; } isRecording() { return this._isRecording; } static get mediaType() { return mediaType; } static get EVENTS() { return _EVENTS; } getTimeNow() { return new Date().toTimeString().split(' ')[0]; } getId(id) { return document.getElementById(id); } getEcN(cn) { return document.getElementsByClassName(cn); } async getRoomInfo() { let room_info = await this.socket.request('getRoomInfo'); return room_info; } refreshParticipantsCount() { this.socket.emit('refreshParticipantsCount'); } getPeerAudioBtn(peer_id) { return this.getId(peer_id + '__audio'); } getPeerHandBtn(peer_id) { return this.getId(peer_id + '__hand'); } // #################################################### // UTILITY // #################################################### async sound(name) { let sound = '../sounds/' + name + '.wav'; let audio = new Audio(sound); try { await audio.play(); } catch (err) { return false; } } userLog(icon, message, position, timer = 5000) { const Toast = Swal.mixin({ background: swalBackground, toast: true, position: position, showConfirmButton: false, timer: timer, }); Toast.fire({ icon: icon, title: message, }); } thereIsParticipants() { // console.log('participantsCount ---->', participantsCount); if (this.consumers.size > 0 || participantsCount > 1) { return true; } return false; } // #################################################### // MY SETTINGS // #################################################### toggleMySettings() { let mySettings = this.getId('mySettings'); mySettings.style.top = '50%'; mySettings.style.left = '50%'; mySettings.classList.toggle('show'); this.isMySettingsOpen = this.isMySettingsOpen ? false : true; } openTab(evt, tabName) { let i, tabcontent, tablinks; tabcontent = this.getEcN('tabcontent'); for (i = 0; i < tabcontent.length; i++) { tabcontent[i].style.display = 'none'; } tablinks = this.getEcN('tablinks'); for (i = 0; i < tablinks.length; i++) { tablinks[i].className = tablinks[i].className.replace(' active', ''); } this.getId(tabName).style.display = 'block'; evt.currentTarget.className += ' active'; } changeBtnsBarPosition(position) { switch (position) { case 'vertical': document.documentElement.style.setProperty('--btns-top', '50%'); document.documentElement.style.setProperty('--btns-right', '0%'); document.documentElement.style.setProperty('--btns-left', '10px'); document.documentElement.style.setProperty('--btns-margin-left', '0px'); document.documentElement.style.setProperty('--btns-width', '60px'); document.documentElement.style.setProperty('--btns-flex-direction', 'column'); break; case 'horizontal': document.documentElement.style.setProperty('--btns-top', '95%'); document.documentElement.style.setProperty('--btns-right', '25%'); document.documentElement.style.setProperty('--btns-left', '50%'); document.documentElement.style.setProperty('--btns-margin-left', '-160px'); document.documentElement.style.setProperty('--btns-width', '320px'); document.documentElement.style.setProperty('--btns-flex-direction', 'row'); break; } } // #################################################### // FULL SCREEN // #################################################### toggleFullScreen(elem = null) { let el = elem ? elem : document.documentElement; document.fullscreenEnabled = document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled; document.exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen; el.requestFullscreen = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullScreen; if (document.fullscreenEnabled) { document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement ? document.exitFullscreen() : el.requestFullscreen(); } if (elem == null) this.isVideoOnFullScreen = document.fullscreenEnabled; } handleFS(elemId, fsId) { let videoPlayer = this.getId(elemId); let btnFs = this.getId(fsId); this.setTippy(fsId, 'Full screen', 'top'); btnFs.addEventListener('click', () => { videoPlayer.style.pointerEvents = this.isVideoOnFullScreen ? 'auto' : 'none'; this.toggleFullScreen(videoPlayer); this.isVideoOnFullScreen = this.isVideoOnFullScreen ? false : true; }); videoPlayer.addEventListener('click', () => { if ((this.isMobileDevice && this.isVideoOnFullScreen) || !this.isMobileDevice) { videoPlayer.style.pointerEvents = this.isVideoOnFullScreen ? 'auto' : 'none'; this.toggleFullScreen(videoPlayer); this.isVideoOnFullScreen = this.isVideoOnFullScreen ? false : true; } }); videoPlayer.addEventListener('fullscreenchange', (e) => { if (!document.fullscreenElement) { videoPlayer.style.pointerEvents = 'auto'; this.isVideoOnFullScreen = false; } }); videoPlayer.addEventListener('webkitfullscreenchange', (e) => { if (!document.webkitIsFullScreen) { videoPlayer.style.pointerEvents = 'auto'; this.isVideoOnFullScreen = false; } }); } // #################################################### // TAKE SNAPSHOT // #################################################### handleTS(elemId, tsId) { let videoPlayer = this.getId(elemId); let btnTs = this.getId(tsId); btnTs.addEventListener('click', () => { this.sound('snapshot'); let context, canvas, width, height, dataURL; width = videoPlayer.videoWidth; height = videoPlayer.videoHeight; canvas = canvas || document.createElement('canvas'); canvas.width = width; canvas.height = height; context = canvas.getContext('2d'); context.drawImage(videoPlayer, 0, 0, width, height); dataURL = canvas.toDataURL('image/png'); console.log(dataURL); saveDataToFile(dataURL, getDataTimeString() + '-SNAPSHOT.png'); }); } // #################################################### // DRAGGABLE // #################################################### makeDraggable(elmnt, dragObj) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; if (dragObj) { dragObj.onmousedown = dragMouseDown; } else { elmnt.onmousedown = dragMouseDown; } function dragMouseDown(e) { e = e || window.event; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; elmnt.style.top = elmnt.offsetTop - pos2 + 'px'; elmnt.style.left = elmnt.offsetLeft - pos1 + 'px'; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } // #################################################### // CHAT // #################################################### toggleChat() { let chatRoom = this.getId('chatRoom'); if (this.isChatOpen == false) { chatRoom.style.display = 'block'; chatRoom.style.top = '50%'; chatRoom.style.left = '50%'; this.sound('open'); this.isChatOpen = true; } else { chatRoom.style.display = 'none'; this.isChatOpen = false; } } toggleChatEmoji() { this.getId('chatEmoji').classList.toggle('show'); this.isChatEmojiOpen = this.isChatEmojiOpen ? false : true; this.getId('chatEmojiButton').style.color = this.isChatEmojiOpen ? '#FFFF00' : '#FFFFFF'; } sendMessage() { if (!this.thereIsParticipants()) { chatMessage.value = ''; this.userLog('info', 'No participants in the room', 'top-end'); return; } let peer_msg = this.formatMsg(chatMessage.value); if (!peer_msg) return; let data = { peer_name: this.peer_name, to_peer_id: 'all', peer_msg: peer_msg, }; console.log('Send message:', data); this.socket.emit('message', data); this.setMsgAvatar('right', this.peer_name); this.appendMessage('right', this.rightMsgAvatar, this.peer_name, peer_msg, 'all'); chatMessage.value = ''; } sendMessageTo(to_peer_id) { if (!this.thereIsParticipants()) { chatMessage.value = ''; this.userLog('info', 'No participants in the room expect you', 'top-end'); return; } Swal.fire({ background: swalBackground, position: 'center', imageUrl: image.message, input: 'text', inputPlaceholder: '💬 Enter your message...', showCancelButton: true, confirmButtonText: `Send`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }).then((result) => { let peer_msg = this.formatMsg(result.value); if (!peer_msg) return; let data = { peer_name: this.peer_name, to_peer_id: to_peer_id, peer_msg: peer_msg, }; console.log('Send message:', data); this.socket.emit('message', data); this.setMsgAvatar('right', this.peer_name); this.appendMessage('right', this.rightMsgAvatar, this.peer_name, peer_msg, to_peer_id); }); } showMessage(data) { if (!this.isChatOpen) this.toggleChat(); this.setMsgAvatar('left', data.peer_name); this.appendMessage('left', this.leftMsgAvatar, data.peer_name, data.peer_msg, data.to_peer_id); this.sound('message'); } setMsgAvatar(avatar, peerName) { let avatarImg = cfg.msgAvatar + '?name=' + peerName + '&size=32' + '&background=random&rounded=true'; avatar === 'left' ? (this.leftMsgAvatar = avatarImg) : (this.rightMsgAvatar = avatarImg); } appendMessage(side, img, from, msg, to) { let time = this.getTimeNow(); let msgBubble = to == 'all' ? 'msg-bubble' : 'msg-bubble-private'; let message = to == 'all' ? msg : msg + '
(private message)'; let msgHTML = `
${from}
${time}
${message}
`; this.collectMessages(time, from, msg); chatMsger.insertAdjacentHTML('beforeend', msgHTML); chatMsger.scrollTop += 500; } formatMsg(message) { let urlRegex = /(https?:\/\/[^\s]+)/g; return message.replace(urlRegex, (url) => { if (message.match(/\.(jpeg|jpg|gif|png|tiff|bmp)$/)) return 'img'; return '' + url + ''; }); } collectMessages(time, from, msg) { this.chatMessages.push({ time: time, from: from, msg: msg, }); } chatClean() { Swal.fire({ background: swalBackground, position: 'center', title: 'Clean up chat Messages?', imageUrl: image.delete, showDenyButton: true, confirmButtonText: `Yes`, denyButtonText: `No`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }).then((result) => { if (result.isConfirmed) { let msgs = chatMsger.firstChild; while (msgs) { chatMsger.removeChild(msgs); msgs = chatMsger.firstChild; } this.chatMessages = []; } }); } chatSave() { if (this.chatMessages.length === 0) { userLog('info', 'No chat messages to save', 'top-end'); return; } const newDate = new Date(); const date = newDate.toISOString().split('T')[0]; const time = newDate.toTimeString().split(' ')[0]; let a = document.createElement('a'); a.href = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(this.chatMessages, null, 1)); a.download = `${date}-${time}` + '-CHAT.txt'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); } // #################################################### // RECORDING // #################################################### 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); }); } startRecording() { recordedBlobs = []; let options = this.getSupportedMimeTypes(); console.log('MediaRecorder supported options', options); options = { mimeType: options[0] }; try { if (this.isMobileDevice) { // on mobile devices recording camera + audio let newStream = this.getNewStream(this.localVideoStream, this.localAudioStream); this.mediaRecorder = new MediaRecorder(newStream, options); console.log('Created MediaRecorder', this.mediaRecorder, 'with options', options); this.getId('swapCameraButton').className = 'hidden'; this._isRecording = true; this.handleMediaRecorder(); this.event(_EVENTS.startRec); this.sound('recStart'); } else { // on desktop devices recording screen/window... + audio const constraints = { video: true }; navigator.mediaDevices .getDisplayMedia(constraints) .then((screenStream) => { this.recScreenStream = this.getNewStream(screenStream, this.localAudioStream); this.mediaRecorder = new MediaRecorder(this.recScreenStream, options); console.log('Created MediaRecorder', this.mediaRecorder, 'with options', options); this._isRecording = true; this.handleMediaRecorder(); this.event(_EVENTS.startRec); this.sound('recStart'); }) .catch((err) => { console.error('Error Unable to recording the screen + audio', err); this.userLog('error', 'Unable to recording the screen + audio reason: ' + err, 'top-end'); }); } } catch (err) { console.error('Exception while creating MediaRecorder: ', err); this.userLog('error', "Can't start stream recording reason: " + err, 'top-end'); return; } } getNewStream(videoStream, audioStream) { let newStream = null; let videoStreamTrack = videoStream ? videoStream.getVideoTracks()[0] : undefined; let audioStreamTrack = audioStream ? audioStream.getAudioTracks()[0] : undefined; if (videoStreamTrack && audioStreamTrack) { newStream = new MediaStream([videoStreamTrack, audioStreamTrack]); } else if (videoStreamTrack) { newStream = new MediaStream([videoStreamTrack]); } else if (audioStreamTrack) { newStream = new MediaStream([audioStreamTrack]); } return newStream; } handleMediaRecorder() { this.mediaRecorder.start(); this.mediaRecorder.addEventListener('start', this.handleMediaRecorderStart); this.mediaRecorder.addEventListener('dataavailable', this.handleMediaRecorderData); this.mediaRecorder.addEventListener('stop', this.handleMediaRecorderStop); } handleMediaRecorderStart(evt) { console.log('MediaRecorder started: ', evt); } handleMediaRecorderData(evt) { console.log('MediaRecorder data: ', evt); if (evt.data && evt.data.size > 0) recordedBlobs.push(evt.data); } handleMediaRecorderStop(evt) { try { console.log('MediaRecorder stopped: ', evt); console.log('MediaRecorder Blobs: ', recordedBlobs); const newDate = new Date(); const date = newDate.toISOString().split('T')[0]; const time = newDate.toTimeString().split(' ')[0]; const type = recordedBlobs[0].type.includes('mp4') ? 'mp4' : 'webm'; const blob = new Blob(recordedBlobs, { type: 'video/' + type }); const recFileName = `${date}-${time}` + '-REC.' + type; console.log('MediaRecorder Download Blobs'); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = recFileName; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); console.log(`🔴 Recording FILE: ${recFileName} done 👍`); } catch (ex) { console.warn('Recording save failed', ex); } } pauseRecording() { this._isRecording = false; this.mediaRecorder.pause(); this.event(_EVENTS.pauseRec); } resumeRecording() { this._isRecording = true; this.mediaRecorder.resume(); this.event(_EVENTS.resumeRec); } stopRecording() { this._isRecording = false; this.mediaRecorder.stop(); if (this.recScreenStream) { this.recScreenStream.getTracks().forEach((track) => { if (track.kind === 'video') track.stop(); }); } if (this.isMobileDevice) this.getId('swapCameraButton').className = ''; this.getId('recordingStatus').innerHTML = '🔴 REC 0s'; this.event(_EVENTS.stopRec); this.sound('recStop'); } // #################################################### // FILE SHARING // #################################################### selectFileToShare(peer_id, broadcast = true) { this.sound('open'); Swal.fire({ allowOutsideClick: false, background: swalBackground, imageAlt: 'mirotalksfu-file-sharing', imageUrl: image.share, position: 'center', title: 'Share file', input: 'file', inputAttributes: { accept: this.fileSharingInput, 'aria-label': 'Select file', }, showDenyButton: true, confirmButtonText: `Send`, denyButtonText: `Cancel`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }).then((result) => { if (result.isConfirmed) { this.fileToSend = result.value; if (this.fileToSend && this.fileToSend.size > 0) { if (!this.thereIsParticipants()) { userLog('info', 'No participants detected', 'top-end'); return; } // send some metadata about our file to peers in the room this.socket.emit('fileInfo', { peer_id: peer_id, broadcast: broadcast, peer_name: this.peer_name, fileName: this.fileToSend.name, fileSize: this.fileToSend.size, fileType: this.fileToSend.type, }); setTimeout(() => { this.sendFileData(peer_id, broadcast); }, 1000); } else { userLog('error', 'File not selected or empty.', 'top-end'); } } }); } handleFileInfo(data) { this.incomingFileInfo = data; this.incomingFileData = []; this.receiveBuffer = []; this.receivedSize = 0; let fileToReceiveInfo = ' From: ' + this.incomingFileInfo.peer_name + html.newline + ' Incoming file: ' + this.incomingFileInfo.fileName + html.newline + ' File type: ' + this.incomingFileInfo.fileType + html.newline + ' File size: ' + this.bytesToSize(this.incomingFileInfo.fileSize); receiveFileInfo.innerHTML = fileToReceiveInfo; receiveFileDiv.style.display = 'inline'; receiveProgress.max = this.incomingFileInfo.fileSize; this.userLog('info', fileToReceiveInfo, 'top-end'); this.receiveInProgress = true; } sendFileData(peer_id, broadcast) { console.log('Send file ', { name: this.fileToSend.name, size: this.bytesToSize(this.fileToSend.size), type: this.fileToSend.type, }); this.sendInProgress = true; sendFileInfo.innerHTML = 'File name: ' + this.fileToSend.name + html.newline + 'File type: ' + this.fileToSend.type + html.newline + 'File size: ' + this.bytesToSize(this.fileToSend.size) + html.newline; sendFileDiv.style.display = 'inline'; sendProgress.max = this.fileToSend.size; this.fileReader = new FileReader(); let offset = 0; this.fileReader.addEventListener('error', (err) => console.error('fileReader error', err)); this.fileReader.addEventListener('abort', (e) => console.log('fileReader aborted', e)); this.fileReader.addEventListener('load', (e) => { if (!this.sendInProgress) return; let data = { peer_id: peer_id, broadcast: broadcast, fileData: e.target.result, }; this.sendFSData(data); offset += data.fileData.byteLength; sendProgress.value = offset; sendFilePercentage.innerHTML = 'Send progress: ' + ((offset / this.fileToSend.size) * 100).toFixed(2) + '%'; // send file completed if (offset === this.fileToSend.size) { this.sendInProgress = false; sendFileDiv.style.display = 'none'; userLog('success', 'The file ' + this.fileToSend.name + ' was sent successfully.', 'top-end'); } if (offset < this.fileToSend.size) readSlice(offset); }); const readSlice = (o) => { const slice = this.fileToSend.slice(offset, o + this.chunkSize); this.fileReader.readAsArrayBuffer(slice); }; readSlice(0); } sendFSData(data) { if (data) this.socket.emit('file', data); } abortFileTransfer() { if (this.fileReader && this.fileReader.readyState === 1) { this.fileReader.abort(); sendFileDiv.style.display = 'none'; this.sendInProgress = false; this.socket.emit('fileAbort', { peer_name: this.peer_name, }); } } hideFileTransfer() { receiveFileDiv.style.display = 'none'; } handleFileAbort(data) { this.receiveBuffer = []; this.incomingFileData = []; this.receivedSize = 0; this.receiveInProgress = false; receiveFileDiv.style.display = 'none'; console.log(data.peer_name + ' aborted the file transfer'); userLog('info', data.peer_name + ' ⚠️ aborted the file transfer', 'top-end'); } handleFile(data) { if (!this.receiveInProgress) return; this.receiveBuffer.push(data.fileData); this.receivedSize += data.fileData.byteLength; receiveProgress.value = this.receivedSize; receiveFilePercentage.innerHTML = 'Receive progress: ' + ((this.receivedSize / this.incomingFileInfo.fileSize) * 100).toFixed(2) + '%'; if (this.receivedSize === this.incomingFileInfo.fileSize) { receiveFileDiv.style.display = 'none'; this.incomingFileData = this.receiveBuffer; this.receiveBuffer = []; this.endFileDownload(); } } endFileDownload() { this.sound('download'); // save received file into Blob const blob = new Blob(this.incomingFileData); const file = this.incomingFileInfo.fileName; this.incomingFileData = []; // if file is image, show the preview if (isImageURL(this.incomingFileInfo.fileName)) { const reader = new FileReader(); reader.onload = (e) => { Swal.fire({ allowOutsideClick: false, background: swalBackground, position: 'center', title: 'Received file', text: this.incomingFileInfo.fileName + ' size ' + this.bytesToSize(this.incomingFileInfo.fileSize), imageUrl: e.target.result, imageAlt: 'mirotalksfu-file-img-download', showDenyButton: true, confirmButtonText: `Save`, denyButtonText: `Cancel`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }).then((result) => { if (result.isConfirmed) this.saveBlobToFile(blob, file); }); }; // blob where is stored downloaded file reader.readAsDataURL(blob); } else { // not img file Swal.fire({ allowOutsideClick: false, background: swalBackground, position: 'center', title: 'Received file', text: this.incomingFileInfo.fileName + ' size ' + this.bytesToSize(this.incomingFileInfo.fileSize), showDenyButton: true, confirmButtonText: `Save`, denyButtonText: `Cancel`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }).then((result) => { if (result.isConfirmed) this.saveBlobToFile(blob, file); }); } } saveBlobToFile(blob, file) { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = file; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); } bytesToSize(bytes) { let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes == 0) return '0 Byte'; let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]; } // #################################################### // YOUTUBE SHARE VIDEO // #################################################### youTubeShareVideo() { this.sound('open'); Swal.fire({ background: swalBackground, position: 'center', imageUrl: image.youtube, title: 'Share YouTube Video', text: 'Past YouTube video URL', input: 'text', showCancelButton: true, confirmButtonText: `Share`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }).then((result) => { if (result.value) { if (!this.thereIsParticipants()) { userLog('info', 'No participants detected', 'top-end'); return; } // https://www.youtube.com/watch?v=RT6_Id5-7-s let you_tube_url = this.getYoutubeEmbed(result.value); if (you_tube_url) { let data = { peer_name: this.peer_name, you_tube_url: you_tube_url, action: 'open', }; console.log('YouTube video URL: ', you_tube_url); this.socket.emit('youTubeAction', data); this.openYouTube(data); } else { this.userLog('error', 'Not valid YouTube URL', 'top-end'); } } }); } getYoutubeEmbed(url) { let regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; let match = url.match(regExp); return match && match[7].length == 11 ? 'https://www.youtube.com/embed/' + match[7] + '?autoplay=1' : false; } youTubeAction(data) { let peer_name = data.peer_name; let action = data.action; switch (action) { case 'open': this.userLog('info', `${peer_name} opened the YouTube video`, 'top-end'); this.openYouTube(data); break; case 'close': this.userLog('info', `${peer_name} closed the YouTube video`, 'top-end'); this.closeYouTube(); break; } } openYouTube(data) { let d, iframe; let peer_name = data.peer_name; let you_tube_url = data.you_tube_url; this.closeYouTube(); show(youTubeCloseBtn); d = document.createElement('div'); d.className = 'Camera'; d.id = '__youTube'; iframe = document.createElement('iframe'); iframe.setAttribute('id', '__youTubeIframe'); iframe.setAttribute('title', peer_name); iframe.setAttribute('width', '100%'); iframe.setAttribute('height', '100%'); iframe.setAttribute('src', you_tube_url); iframe.setAttribute( 'allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', ); iframe.setAttribute('frameborder', '0'); iframe.setAttribute('allowfullscreen', true); d.appendChild(iframe); this.videoMediaContainer.appendChild(d); handleAspectRatio(); console.log('[openYouTube] Video-element-count', this.videoMediaContainer.childElementCount); this.sound('joined'); } closeYouTube(emit = false) { if (emit) { let data = { peer_name: this.peer_name, action: 'close', }; this.socket.emit('youTubeAction', data); } let youTubeDiv = this.getId('__youTube'); if (youTubeDiv) { hide(youTubeCloseBtn); youTubeDiv.parentNode.removeChild(youTubeDiv); handleAspectRatio(); console.log('[closeYouTube] Video-element-count', this.videoMediaContainer.childElementCount); this.sound('left'); } } // #################################################### // ROOM ACTION // #################################################### roomAction(action, emit = true) { let data = { action: action, password: null, }; if (emit) { switch (action) { case 'lock': Swal.fire({ allowOutsideClick: false, allowEscapeKey: false, background: swalBackground, imageUrl: image.locked, input: 'text', inputPlaceholder: 'Set Room password', confirmButtonText: `OK`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, inputValidator: (pwd) => { if (!pwd) return 'Please enter the Room password'; this.RoomPassword = pwd; }, }).then(() => { data.password = this.RoomPassword; this.socket.emit('roomAction', data); this.roomStatus(action); }); break; case 'unlock': this.socket.emit('roomAction', data); this.roomStatus(action); break; } } else { this.roomStatus(action); } } roomStatus(action) { switch (action) { case 'lock': this.sound('locked'); this.event(_EVENTS.roomLock); this.userLog('info', '🔒 LOCKED the room by the password', 'top-end'); break; case 'unlock': this.event(_EVENTS.roomUnlock); this.userLog('info', '🔓 UNLOCKED the room', 'top-end'); break; } } roomPassword(data) { switch (data.password) { case 'OK': this.joinAllowed(data.room); break; case 'KO': this.roomIsLocked(); break; } } // #################################################### // HANDLE ROOM ACTION // #################################################### unlockTheRoom() { Swal.fire({ allowOutsideClick: false, allowEscapeKey: false, background: swalBackground, imageUrl: image.locked, title: 'Oops, Room is Locked', input: 'text', inputPlaceholder: 'Enter the Room password', confirmButtonText: `OK`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, inputValidator: (pwd) => { if (!pwd) return 'Please enter the Room password'; this.RoomPassword = pwd; }, }).then(() => { let data = { action: 'checkPassword', password: this.RoomPassword, }; this.socket.emit('roomAction', data); }); } roomIsLocked() { this.sound('eject'); this.event(_EVENTS.roomLock); console.log('Room is Locked, try with another one'); Swal.fire({ allowOutsideClick: false, background: swalBackground, position: 'center', imageUrl: image.locked, title: 'Oops, Wrong Room Password', text: 'The room is locked, try with another one.', showDenyButton: false, confirmButtonText: `Ok`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }).then((result) => { if (result.isConfirmed) this.exit(); }); } // #################################################### // HANDLE AUDIO VOLUME // #################################################### handleAudioVolume(data) { let peerId = data.peer_id; let producerAudioBtn = this.getId(peerId + '_audio'); let consumerAudioBtn = this.getId(peerId + '__audio'); let pbProducer = this.getId(peerId + '_pitchBar'); let pbConsumer = this.getId(peerId + '__pitchBar'); let audioVolume = data.audioVolume * 10; //10-100 // console.log('Active speaker', { peer_id: peerId, audioVolume: audioVolume }); if (audioVolume > 40) { if (producerAudioBtn) producerAudioBtn.style.color = 'orange'; if (consumerAudioBtn) consumerAudioBtn.style.color = 'orange'; if (pbProducer) pbProducer.style.backgroundColor = 'orange'; if (pbConsumer) pbConsumer.style.backgroundColor = 'orange'; } else { if (producerAudioBtn) producerAudioBtn.style.color = 'lime'; if (consumerAudioBtn) consumerAudioBtn.style.color = 'lime'; if (pbProducer) pbProducer.style.backgroundColor = 'lime'; if (pbConsumer) pbConsumer.style.backgroundColor = 'lime'; } if (pbProducer) pbProducer.style.height = audioVolume + '%'; if (pbConsumer) pbConsumer.style.height = audioVolume + '%'; setTimeout(function () { if (producerAudioBtn) producerAudioBtn.style.color = 'white'; if (consumerAudioBtn) consumerAudioBtn.style.color = 'white'; if (pbProducer) pbProducer.style.height = '0%'; if (pbConsumer) pbConsumer.style.height = '0%'; }, 2000); } // #################################################### // PEER ACTION // #################################################### peerAction(from_peer_name, id, action, emit = true, broadcast = false) { const words = id.split('___'); let peer_id = words[0]; if (emit) { let data = { from_peer_name: this.peer_name, peer_id: peer_id, action: action, broadcast: broadcast, }; if (!this.thereIsParticipants()) { this.userLog('info', 'No participants detected', 'top-end'); return; } this.confirmPeerAction(action, data); } else { switch (action) { case 'eject': if (peer_id === this.peer_id || broadcast) { this.sound(action); this.peerActionProgress(from_peer_name, 'Will eject you from the room', 5000, action); } break; case 'mute': if (peer_id === this.peer_id || broadcast) { this.closeProducer(mediaType.audio); this.updatePeerInfo(this.peer_name, this.peer_id, 'audio', false); this.userLog( 'warning', from_peer_name + ' ' + _PEER.audioOff + ' has closed yours audio', 'top-end', 10000, ); } break; case 'hide': if (peer_id === this.peer_id || broadcast) { this.closeProducer(mediaType.video); this.userLog( 'warning', from_peer_name + ' ' + _PEER.videoOff + ' has closed yours video', 'top-end', 10000, ); } break; // ... } } } peerActionProgress(tt, msg, time, action = 'na') { Swal.fire({ allowOutsideClick: false, background: swalBackground, icon: action == 'eject' ? 'warning' : 'success', title: tt, text: msg, timer: time, timerProgressBar: true, didOpen: () => { Swal.showLoading(); }, }).then(() => { switch (action) { case 'refresh': getRoomParticipants(true); break; case 'eject': this.exit(); break; } }); } confirmPeerAction(action, data) { switch (action) { case 'eject': let ejectConfirmed = false; let whoEject = data.broadcast ? 'All participants' : 'current participant'; Swal.fire({ background: swalBackground, position: 'center', imageUrl: data.broadcast ? image.users : image.user, title: 'Eject ' + whoEject + ' excpect yourself?', showDenyButton: true, confirmButtonText: `Yes`, denyButtonText: `No`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }) .then((result) => { if (result.isConfirmed) { ejectConfirmed = true; if (!data.broadcast) { let peer = this.getId(data.peer_id); if (peer) { peer.parentNode.removeChild(peer); participantsCount--; refreshParticipantsCount(participantsCount); this.socket.emit('peerAction', data); } } else { let actionButton = this.getId(action + 'AllButton'); if (actionButton) actionButton.style.display = 'none'; participantsCount = 1; refreshParticipantsCount(participantsCount); this.socket.emit('peerAction', data); } } }) .then(() => { if (ejectConfirmed) this.peerActionProgress(action, 'In progress, wait...', 6000, 'refresh'); }); break; case 'mute': case 'hide': let muteHideConfirmed = false; let whoMuteHide = data.broadcast ? 'everyone' : 'current participant'; Swal.fire({ background: swalBackground, position: 'center', imageUrl: action == 'mute' ? image.mute : image.hide, title: action == 'mute' ? 'Mute ' + whoMuteHide + ' excpect yourself?' : 'Hide ' + whoMuteHide + ' except yourself?', text: action == 'mute' ? "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: `Yes`, denyButtonText: `No`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, }) .then((result) => { if (result.isConfirmed) { muteHideConfirmed = true; if (!data.broadcast) { switch (action) { case 'mute': let peerAudioButton = this.getId(data.peer_id + '___pAudio'); if (peerAudioButton) peerAudioButton.innerHTML = _PEER.audioOff; break; case 'hide': let peerVideoButton = this.getId(data.peer_id + '___pVideo'); if (peerVideoButton) peerVideoButton.innerHTML = _PEER.videoOff; } this.socket.emit('peerAction', data); } else { let actionButton = this.getId(action + 'AllButton'); if (actionButton) actionButton.style.display = 'none'; this.socket.emit('peerAction', data); } } }) .then(() => { if (muteHideConfirmed) this.peerActionProgress(action, 'In progress, wait...', 2000, 'refresh'); }); break; } } // #################################################### // SEARCH PEER FILTER // #################################################### searchPeer() { let input, filter, table, tr, td, i, txtValue; input = this.getId('searchParticipants'); filter = input.value.toUpperCase(); table = this.getId('myTable'); tr = table.getElementsByTagName('tr'); for (i = 0; i < tr.length; i++) { td = tr[i].getElementsByTagName('td')[0]; if (td) { txtValue = td.textContent || td.innerText; if (txtValue.toUpperCase().indexOf(filter) > -1) { tr[i].style.display = ''; } else { tr[i].style.display = 'none'; } } } } // #################################################### // UPDATE PEER INFO // #################################################### updatePeerInfo(peer_name, peer_id, type, status, emit = true) { if (emit) { switch (type) { case 'audio': this.setIsAudio(peer_id, status); break; case 'video': this.setIsVideo(status); break; case 'hand': this.peer_info.peer_hand = status; let peer_hand = this.getPeerHandBtn(peer_id); if (status) { if (peer_hand) peer_hand.style.display = 'flex'; this.event(_EVENTS.raiseHand); this.sound('raiseHand'); } else { if (peer_hand) peer_hand.style.display = 'none'; this.event(_EVENTS.lowerHand); } break; } let data = { peer_name: peer_name, peer_id: peer_id, type: type, status: status, }; this.socket.emit('updatePeerInfo', data); } else { switch (type) { case 'audio': this.setIsAudio(peer_id, status); break; case 'video': this.setIsVideo(status); break; case 'hand': let peer_hand = this.getPeerHandBtn(peer_id); if (status) { if (peer_hand) peer_hand.style.display = 'flex'; this.userLog( 'warning', peer_name + ' ' + _PEER.raiseHand + ' has raised the hand', 'top-end', 10000, ); this.sound('raiseHand'); } else { if (peer_hand) peer_hand.style.display = 'none'; } break; } } } checkPeerInfoStatus(peer_info) { let peer_id = peer_info.peer_id; let peer_hand_status = peer_info.peer_hand; if (peer_hand_status) { let peer_hand = this.getPeerHandBtn(peer_id); if (peer_hand) peer_hand.style.display = 'flex'; } //... } popupPeerInfo(id, peer_info) { if (this.debug) { this.setTippy( id, JSON.stringify( peer_info, [ 'peer_name', 'peer_audio', 'peer_video', 'peer_hand', 'is_mobile_device', 'os_name', 'os_version', 'browser_name', 'browser_version', ], 2, ), 'top-start', ); } } }