diff --git a/app/src/server.js b/app/src/server.js index bf21cb48..8552d673 100755 --- a/app/src/server.js +++ b/app/src/server.js @@ -493,6 +493,7 @@ io.sockets.on('connect', async (socket) => { let peer_screen_status = config.peer_screen_status; let peer_hand_status = config.peer_hand_status; let peer_rec_status = config.peer_rec_status; + let peer_privacy_status = config.peer_privacy_status; if (channel in socket.channels) { return log.debug('[' + socket.id + '] [Warning] already joined', channel); @@ -519,6 +520,7 @@ io.sockets.on('connect', async (socket) => { peer_screen_status: peer_screen_status, peer_hand_status: peer_hand_status, peer_rec_status: peer_rec_status, + peer_privacy_status: peer_privacy_status, }; log.debug('[Join] - connected peers grp by roomId', peers); @@ -708,6 +710,7 @@ io.sockets.on('connect', async (socket) => { let peer_name = config.peer_name; let element = config.element; let status = config.status; + try { for (let peer_id in peers[room_id]) { if (peers[room_id][peer_id]['peer_name'] == peer_name) { @@ -727,6 +730,9 @@ io.sockets.on('connect', async (socket) => { case 'rec': peers[room_id][peer_id]['peer_rec_status'] = status; break; + case 'privacy': + peers[room_id][peer_id]['peer_privacy_status'] = status; + break; } } } diff --git a/public/css/videoGrid.css b/public/css/videoGrid.css index 690c197e..4974f1b9 100644 --- a/public/css/videoGrid.css +++ b/public/css/videoGrid.css @@ -113,6 +113,26 @@ # Video --------------------------------------------------------------*/ +.videoCircle { + position: absolute; + width: var(--vmi-wh); + height: var(--vmi-wh); + border-radius: 50%; + /* center */ + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; +} + +.videoDefault { + position: absolute; + width: 100%; + height: 100%; + border-radius: '10px'; +} + video { width: 100%; height: 100%; diff --git a/public/js/client.js b/public/js/client.js index b4872c53..0d27c22b 100644 --- a/public/js/client.js +++ b/public/js/client.js @@ -141,6 +141,8 @@ let needToEnableMyAudio = false; // On screen sharing end, check if need to enab let initEnumerateDevicesFailed = false; // Check if user webcam and audio init is failed +let isVideoPrivacyActive = false; // Video circle for privacy + let myPeerId; // socket.id let peerInfo = {}; // Some peer info let userAgent; // User agent info @@ -1071,6 +1073,7 @@ async function joinToChannel() { peer_screen_status: myScreenStatus, peer_hand_status: myHandStatus, peer_rec_status: isRecScreenStream, + peer_privacy_status: isVideoPrivacyActive, }); } @@ -1566,7 +1569,6 @@ async function initEnumerateDevices() { background: '#000000', position: 'center', imageUrl: camMicOff, - //icon: 'warning', title: 'Camera and microphone not allowed', text: "Meet needs access to the camera and microphone. Click the locked camera and microphone icon in your browser's address bar, before to join room.", showDenyButton: false, @@ -1778,6 +1780,7 @@ async function loadLocalMedia(stream) { 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'); @@ -1798,6 +1801,10 @@ async function loadLocalMedia(stream) { myHandStatusIcon.className = 'fas fa-hand-paper pulsate'; myHandStatusIcon.style.setProperty('color', 'rgb(0, 255, 0)'); + // my privacy button + myPrivacyBtn.setAttribute('id', 'myPrivacyBtn'); + myPrivacyBtn.className = 'far fa-circle'; + // my video status element myVideoStatusIcon.setAttribute('id', 'myVideoStatusIcon'); myVideoStatusIcon.className = 'fas fa-video'; @@ -1822,6 +1829,7 @@ async function loadLocalMedia(stream) { setTippy(myCountTime, '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'); @@ -1855,6 +1863,7 @@ async function loadLocalMedia(stream) { myVideoNavBar.appendChild(myVideoToImgBtn); } + myVideoNavBar.appendChild(myPrivacyBtn); myVideoNavBar.appendChild(myAudioStatusIcon); myVideoNavBar.appendChild(myVideoStatusIcon); myVideoNavBar.appendChild(myHandStatusIcon); @@ -1909,7 +1918,10 @@ async function loadLocalMedia(stream) { handleVideoToImg('myVideo', 'myVideoToImgBtn'); } + handleVideoPrivacyBtn('myVideo', 'myPrivacyBtn'); + handleVideoPinUnpin('myVideo', 'myVideoPinBtn', 'myVideoWrap', 'myVideo'); + refreshMyVideoAudioStatus(localMediaStream); if (!useVideo) { @@ -1964,6 +1976,7 @@ async function loadRemoteMediaStream(stream, peers, peer_id) { let peer_screen_status = peers[peer_id]['peer_screen_status']; let peer_hand_status = peers[peer_id]['peer_hand_status']; let peer_rec_status = peers[peer_id]['peer_rec_status']; + let peer_privacy_status = peers[peer_id]['peer_privacy_status']; remoteMediaStream = stream; @@ -1996,6 +2009,7 @@ async function loadRemoteMediaStream(stream, peers, peer_id) { 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)'); @@ -2142,6 +2156,11 @@ async function loadRemoteMediaStream(stream, peers, peer_id) { handlePeerKickOutBtn(peer_id); } + if (peer_privacy_status) { + // set video privacy true + setVideoPrivacyStatus(remoteMedia.id, peer_privacy_status); + } + // refresh remote peers avatar name setPeerAvatarImgName(peer_id + '_avatar', peer_name, useAvatarApi); // refresh remote peers hand icon status and title @@ -2459,6 +2478,42 @@ function handleFileDragAndDrop(elemId, peer_id, itsMe = false) { }); } +/** + * Handle video privacy button click event + * @param {string} videoId + * @param {boolean} privacyBtnId + */ +function handleVideoPrivacyBtn(videoId, privacyBtnId) { + let video = getId(videoId); + let privacyBtn = getId(privacyBtnId); + if (useVideo && video && privacyBtn) { + privacyBtn.addEventListener('click', () => { + playSound('click'); + isVideoPrivacyActive = !isVideoPrivacyActive; + setVideoPrivacyStatus(videoId, isVideoPrivacyActive); + emitPeerStatus('privacy', isVideoPrivacyActive); + }); + } else { + if (privacyBtn) privacyBtn.style.display = 'none'; + } +} + +/** + * Set video privacy status + * @param {string} peerVideoId + * @param {boolean} peerPrivacyActive + */ +function setVideoPrivacyStatus(peerVideoId, peerPrivacyActive) { + let video = getId(peerVideoId); + if (peerPrivacyActive) { + video.classList.remove('videoDefault'); + video.classList.add('videoCircle'); + } else { + video.classList.remove('videoCircle'); + video.classList.add('videoDefault'); + } +} + /** * Handle video pin/unpin * @param {string} elemId video id @@ -2474,6 +2529,7 @@ function handleVideoPinUnpin(elemId, pnId, camId, peerId) { let videoPinMediaContainer = getId('videoPinMediaContainer'); if (btnPn && videoPlayer && cam) { btnPn.addEventListener('click', () => { + playSound('click'); isVideoPinned = !isVideoPinned; if (isVideoPinned) { videoPlayer.style.objectFit = 'contain'; @@ -3826,6 +3882,8 @@ async function toggleScreenSharing() { let screenMediaPromise = null; + let myPrivacyBtn = getId('myPrivacyBtn'); + try { if (!isScreenStreaming) { // on screen sharing start @@ -3835,6 +3893,8 @@ async function toggleScreenSharing() { screenMediaPromise = await navigator.mediaDevices.getUserMedia(getAudioVideoConstraints()); } if (screenMediaPromise) { + isVideoPrivacyActive = false; + await emitPeerStatus('privacy', isVideoPrivacyActive); isScreenStreaming = !isScreenStreaming; if (isScreenStreaming) { setMyVideoStatusTrue(); @@ -3844,7 +3904,7 @@ async function toggleScreenSharing() { adaptAspectRatio(); } myScreenStatus = isScreenStreaming; - emitPeerStatus('screen', myScreenStatus); + await emitPeerStatus('screen', myScreenStatus); await stopLocalVideoTrack(); await refreshMyLocalStream(screenMediaPromise); await refreshMyStreamToPeers(screenMediaPromise); @@ -3852,6 +3912,7 @@ async function toggleScreenSharing() { setScreenSharingStatus(isScreenStreaming); if (myVideoAvatarImage && !useVideo) myVideoAvatarImage.style.display = isScreenStreaming ? 'none' : 'block'; + myPrivacyBtn.style.display = isScreenStreaming ? 'none' : 'inline'; } } catch (err) { console.error('[Error] Unable to share the screen', err); @@ -4016,6 +4077,9 @@ async function refreshMyLocalStream(stream, localAudioTrackChange = false) { // attachMediaStream is a part of the adapter.js library attachMediaStream(myVideo, localMediaStream); // newstream + // refresh video privacy mode + setVideoPrivacyStatus('myVideo', isVideoPrivacyActive); + // on toggleScreenSharing video stop if (useVideo || isScreenStreaming) { stream.getVideoTracks()[0].onended = () => { @@ -5025,7 +5089,7 @@ function handlePeerName(config) { * @param {string} element typo * @param {boolean} status true/false */ -function emitPeerStatus(element, status) { +async function emitPeerStatus(element, status) { sendToServer('peerStatus', { room_id: roomId, peer_name: myPeerName, @@ -5083,7 +5147,7 @@ function setMyVideoStatus(status) { } /** - * Handle peer audio - video - hand status + * Handle peer audio - video - hand - privacy status * @param {object} config data */ function handlePeerStatus(config) { @@ -5103,6 +5167,9 @@ function handlePeerStatus(config) { case 'hand': setPeerHandStatus(peer_id, peer_name, status); break; + case 'privacy': + setVideoPrivacyStatus(peer_id + '_video', status); + break; } } diff --git a/public/sounds/click.mp3 b/public/sounds/click.mp3 new file mode 100644 index 00000000..5483a998 Binary files /dev/null and b/public/sounds/click.mp3 differ