From 129baf9a1623c80eebedb399933d3ae8612aa737 Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Fri, 18 Nov 2022 18:45:15 +0100 Subject: [PATCH] [mirotalk] - add toggle video privacy --- app/src/server.js | 6 ++++ public/css/videoGrid.css | 20 +++++++++++ public/js/client.js | 75 ++++++++++++++++++++++++++++++++++++--- public/sounds/click.mp3 | Bin 0 -> 4178 bytes 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 public/sounds/click.mp3 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 0000000000000000000000000000000000000000..5483a99886b6af3bb8087c6ac0f1abd6cb761cee GIT binary patch literal 4178 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 literal 0 HcmV?d00001