diff --git a/public/Room.html b/public/Room.html index fefa05b..6ab111f 100644 --- a/public/Room.html +++ b/public/Room.html @@ -116,6 +116,8 @@ access to use this app. + + diff --git a/public/css/Room.css b/public/css/Room.css index 48ed050..325c501 100644 --- a/public/css/Room.css +++ b/public/css/Room.css @@ -532,16 +532,6 @@ button:hover { overflow: auto; } -#roomParticipants button { - background: transparent; - color: red; - border-radius: 5px; -} -#roomParticipants button:hover { - color: white; - border-radius: 5px; -} - #roomParticipants table { border-collapse: collapse; width: 100%; diff --git a/public/images/hide.png b/public/images/hide.png new file mode 100644 index 0000000..cb221c2 Binary files /dev/null and b/public/images/hide.png differ diff --git a/public/images/mute.png b/public/images/mute.png new file mode 100644 index 0000000..29cb77a Binary files /dev/null and b/public/images/mute.png differ diff --git a/public/js/Room.js b/public/js/Room.js index 26e7ef5..9af9558 100644 --- a/public/js/Room.js +++ b/public/js/Room.js @@ -4,6 +4,16 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h const RoomURL = window.location.href; +const _PEER = { + audioOn: '', + audioOff: '', + videoOn: '', + videoOff: '', + raiseHand: '', + lowerHand: '', + ejectPeer: '', +}; + const swalBackground = 'linear-gradient(to left, #1f1e1e, #000000)'; const swalImageUrl = '../images/pricing-illustration.svg'; @@ -174,6 +184,7 @@ function getPeerInfo() { peer_name: peer_name, peer_audio: isAudioOn, peer_video: isVideoOn, + peer_hand: false, }; } @@ -389,6 +400,7 @@ function roomIsReady() { show(fullScreenButton); } show(settingsButton); + show(raiseHandButton); if (isAudioAllowed) show(startAudioButton); if (isVideoAllowed) show(startVideoButton); show(participantsButton); @@ -534,6 +546,12 @@ function handleButtons() { swapCameraButton.onclick = () => { rc.closeThenProduce(RoomClient.mediaType.video, null, true); }; + raiseHandButton.onclick = () => { + rc.updatePeerInfo(peer_name, rc.peer_id, 'hand', true); + }; + lowerHandButton.onclick = () => { + rc.updatePeerInfo(peer_name, rc.peer_id, 'hand', false); + }; startAudioButton.onclick = () => { rc.produce(RoomClient.mediaType.audio, microphoneSelect.value); // rc.resumeProducer(RoomClient.mediaType.audio); @@ -633,6 +651,17 @@ function handleRoomClientEvents() { show(startRecButton); stopRecordingTimer(); }); + rc.on(RoomClient.EVENTS.raiseHand, () => { + console.log('Room Client raise hand'); + hide(raiseHandButton); + show(lowerHandButton); + setColor(lowerHandButton, 'green'); + }); + rc.on(RoomClient.EVENTS.lowerHand, () => { + console.log('Room Client lower hand'); + hide(lowerHandButton); + show(raiseHandButton); + }); rc.on(RoomClient.EVENTS.startAudio, () => { console.log('Room Client start audio'); hide(startAudioButton); @@ -720,13 +749,13 @@ function closeNav() { // SHOW LOG // #################################################### -function userLog(icon, message, position) { +function userLog(icon, message, position, timer = 3000) { const Toast = Swal.mixin({ background: swalBackground, toast: true, position: position, showConfirmButton: false, - timer: 3000, + timer: timer, }); Toast.fire({ icon: icon, @@ -755,26 +784,7 @@ async function sound(name) { async function getRoomParticipants() { let room_info = await rc.getRoomInfo(); let peers = new Map(JSON.parse(room_info.peers)); - let table = `
- - - - `; - for (let peer of Array.from(peers.keys())) { - let peer_info = peers.get(peer).peer_info; - let peer_name = peer_info.peer_name; - let peer_id = peer_info.peer_id; - rc.peer_id === peer_id - ? (table += ` - - - `) - : (table += ` - - - `); - } - table += `
👤 ${peer_name} (me)
👤 ${peer_name}
`; + let table = await getParticipantsTable(peers); sound('open'); @@ -783,15 +793,66 @@ async function getRoomParticipants() { position: 'center', title: `Participants ${peers.size}`, html: table, + showCancelButton: true, + confirmButtonText: `Refresh`, + cancelButtonText: `Close`, showClass: { popup: 'animate__animated animate__fadeInDown', }, hideClass: { popup: 'animate__animated animate__fadeOutUp', }, + }).then((result) => { + if (result.isConfirmed) { + getRoomParticipants(); + } }); } +async function getParticipantsTable(peers) { + let table = `
+ + + + + + + + `; + for (let peer of Array.from(peers.keys())) { + let peer_info = peers.get(peer).peer_info; + let peer_name = '👤 ' + peer_info.peer_name; + let peer_audio = peer_info.peer_audio ? _PEER.audioOn : _PEER.audioOff; + let peer_video = peer_info.peer_video ? _PEER.videoOn : _PEER.videoOff; + let peer_hand = peer_info.peer_hand ? _PEER.raiseHand : _PEER.lowerHand; + let peer_eject = _PEER.ejectPeer; + let peer_id = peer_info.peer_id; + if (rc.peer_id === peer_id) { + table += ` + + + + + + + + `; + } else { + table += ` + + + + + + + + `; + } + } + table += `
${peer_name} (me)
${peer_name}
`; + return table; +} + // #################################################### // ABOUT // #################################################### diff --git a/public/js/RoomClient.js b/public/js/RoomClient.js index fa148bc..5f1200f 100644 --- a/public/js/RoomClient.js +++ b/public/js/RoomClient.js @@ -12,6 +12,8 @@ const image = { poster: '../images/loader.gif', delete: '../images/delete.png', locked: '../images/locked.png', + mute: '../images/mute.png', + hide: '../images/hide.png', }; const mediaType = { @@ -28,6 +30,8 @@ const _EVENTS = { pauseRec: 'pauseRec', resumeRec: 'resumeRec', stopRec: 'stopRec', + raiseHand: 'raiseHand', + lowerHand: 'lowerHand', startVideo: 'startVideo', pauseVideo: 'pauseVideo', resumeVideo: 'resumeVideo', @@ -381,6 +385,14 @@ class RoomClient { }.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( 'disconnect', function () { @@ -606,7 +618,7 @@ class RoomClient { let elem, d, p; d = document.createElement('div'); d.className = 'Camera'; - d.id = id + '_d'; + d.id = id + '__d'; elem = document.createElement('video'); elem.setAttribute('id', id); elem.setAttribute('playsinline', true); @@ -614,7 +626,7 @@ class RoomClient { elem.poster = image.poster; this.isMobileDevice || type === mediaType.screen ? (elem.className = '') : (elem.className = 'mirror'); p = document.createElement('p'); - p.id = id + '_name'; + p.id = id + '__name'; p.innerHTML = '👤 ' + this.peer_name + ' (me)'; d.appendChild(elem); d.appendChild(p); @@ -684,11 +696,16 @@ class RoomClient { } let producer_id = this.producerLabel.get(type); - console.log('Close producer', producer_id); - this.socket.emit('producerClosed', { - producer_id, - }); + 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); @@ -696,7 +713,7 @@ class RoomClient { if (type !== mediaType.audio) { let elem = this.getId(producer_id); - let d = this.getId(producer_id + '_d'); + let d = this.getId(producer_id + '__d'); elem.srcObject.getTracks().forEach(function (track) { track.stop(); }); @@ -786,7 +803,7 @@ class RoomClient { case mediaType.video: d = document.createElement('div'); d.className = 'Camera'; - d.id = id + '_d'; + d.id = id + '__d'; elem = document.createElement('video'); elem.setAttribute('id', id); elem.setAttribute('playsinline', true); @@ -794,7 +811,7 @@ class RoomClient { elem.className = ''; elem.poster = image.poster; p = document.createElement('p'); - p.id = id + '_name'; + p.id = id + '__name'; p.innerHTML = '👤 ' + peer_name; d.appendChild(elem); d.appendChild(p); @@ -819,7 +836,7 @@ class RoomClient { removeConsumer(consumer_id) { let elem = this.getId(consumer_id); - let d = this.getId(consumer_id + '_d'); + let d = this.getId(consumer_id + '__d'); elem.srcObject.getTracks().forEach(function (track) { track.stop(); @@ -980,13 +997,13 @@ class RoomClient { } } - userLog(icon, message, position) { + userLog(icon, message, position, timer = 5000) { const Toast = Swal.mixin({ background: swalBackground, toast: true, position: position, showConfirmButton: false, - timer: 5000, + timer: timer, }); Toast.fire({ icon: icon, @@ -1436,45 +1453,136 @@ class RoomClient { // PEER ACTION // #################################################### - peerAction(from_peer_name, peer_id, action, emit = true) { - switch (action) { - case 'eject': - let peer = this.getId(peer_id); - if (peer) peer.parentNode.removeChild(peer); - if (peer_id === this.peer_id) { - this.sound(action); - let timerInterval; - Swal.fire({ - allowOutsideClick: false, - background: swalBackground, - title: from_peer_name, - html: 'Will eject you from the room after milliseconds.', - timer: 5000, - timerProgressBar: true, - didOpen: () => { - Swal.showLoading(); - const b = Swal.getHtmlContainer().querySelector('b'); - timerInterval = setInterval(() => { - b.textContent = Swal.getTimerLeft(); - }, 100); - }, - willClose: () => { - clearInterval(timerInterval); - }, - }).then(() => { - this.exit(); - }); - } - break; - // ... - } + peerAction(from_peer_name, id, action, emit = true, broadcast = false) { + let peer_id = id; + if (emit) { + const words = peer_id.split('__'); + peer_id = words[0]; + + switch (action) { + case 'eject': + let peer = this.getId(peer_id); + if (peer) peer.parentNode.removeChild(peer); + break; + case 'mute': + let peerAudioButton = this.getId(peer_id + '__audio'); + if (peerAudioButton) peerAudioButton.innerHTML = _PEER.audioOff; + break; + case 'hide': + let peerVideoButton = this.getId(peer_id + '__video'); + if (peerVideoButton) peerVideoButton.innerHTML = _PEER.videoOff; + } + let data = { from_peer_name: this.peer_name, peer_id: peer_id, action: action, + broadcast: broadcast, }; this.socket.emit('peerAction', data); + } else { + switch (action) { + case 'eject': + if (peer_id === this.peer_id) { + this.sound(action); + let timerInterval; + Swal.fire({ + allowOutsideClick: false, + background: swalBackground, + title: from_peer_name, + html: 'Will eject you from the room after milliseconds.', + timer: 5000, + timerProgressBar: true, + didOpen: () => { + Swal.showLoading(); + const b = Swal.getHtmlContainer().querySelector('b'); + timerInterval = setInterval(() => { + b.textContent = Swal.getTimerLeft(); + }, 100); + }, + willClose: () => { + clearInterval(timerInterval); + }, + }).then(() => { + this.exit(); + }); + } + break; + case 'mute': + if (peer_id === this.peer_id) { + this.closeProducer(mediaType.audio); + this.userLog( + 'warning', + from_peer_name + ' ' + _PEER.audioOff + ' has closed yours audio', + 'top-end', + 10000, + ); + } + break; + case 'hide': + if (peer_id === this.peer_id) { + this.closeProducer(mediaType.video); + this.userLog( + 'warning', + from_peer_name + ' ' + _PEER.videoOff + ' has closed yours video', + 'top-end', + 10000, + ); + } + break; + // ... + } + } + } + + // #################################################### + // UPDATE PEER INFO + // #################################################### + + updatePeerInfo(peer_name, peer_id, type, status, emit = true) { + if (emit) { + switch (type) { + case 'audio': + this.peer_info.peer_audio = status; + break; + case 'video': + this.peer_info.peer_video = status; + break; + case 'hand': + this.peer_info.peer_hand = status; + if (status) { + this.event(_EVENTS.raiseHand); + this.sound('raiseHand'); + } else { + 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': + break; + case 'video': + break; + case 'hand': + if (status) + this.userLog( + 'warning', + peer_name + ' ' + _PEER.raiseHand + ' has raised the hand', + 'top-end', + 10000, + ); + this.sound('raiseHand'); + break; + } } } @@ -1488,6 +1596,7 @@ class RoomClient { 'peer_name', 'peer_audio', 'peer_video', + 'peer_hand', 'is_mobile_device', 'os_name', 'os_version', diff --git a/public/sounds/raiseHand.wav b/public/sounds/raiseHand.wav new file mode 100644 index 0000000..c2b1352 Binary files /dev/null and b/public/sounds/raiseHand.wav differ diff --git a/src/Peer.js b/src/Peer.js index 5e24aeb..8aeea12 100644 --- a/src/Peer.js +++ b/src/Peer.js @@ -10,11 +10,32 @@ module.exports = class Peer { this.peer_name = data.peer_info.peer_name; this.peer_audio = data.peer_info.peer_audio; this.peer_video = data.peer_info.peer_video; + this.peer_hand = data.peer_info.peer_hand; this.transports = new Map(); this.consumers = new Map(); this.producers = new Map(); } + // #################################################### + // UPDATE PEER INFO + // #################################################### + + updatePeerInfo(data) { + switch (data.type) { + case 'audio': + case 'audioType': + this.peer_info.peer_audio = data.status; + break; + case 'video': + case 'videoType': + this.peer_info.peer_video = data.status; + break; + case 'hand': + this.peer_info.peer_hand = data.status; + break; + } + } + // #################################################### // TRANSPORT // #################################################### diff --git a/src/Server.js b/src/Server.js index 3c6b588..1141d12 100644 --- a/src/Server.js +++ b/src/Server.js @@ -219,7 +219,18 @@ io.on('connection', (socket) => { socket.on('peerAction', (data) => { log.debug('Peer action:', data); - roomList.get(socket.room_id).sendTo(data.peer_id, 'peerAction', data); + if (data.broadcast) { + roomList.get(socket.room_id).broadCast(data.peer_id, 'peerAction', data); + } else { + roomList.get(socket.room_id).sendTo(data.peer_id, 'peerAction', data); + } + }); + + socket.on('updatePeerInfo', (data) => { + log.debug('Peer info update:', data); + // peer_info hand raise Or lower + roomList.get(socket.room_id).getPeers().get(socket.id).updatePeerInfo(data); + roomList.get(socket.room_id).broadCast(socket.id, 'updatePeerInfo', data); }); socket.on('join', (data, cb) => { @@ -290,16 +301,26 @@ io.on('connection', (socket) => { return callback({ error: 'Room not found' }); } + let peer_name = getPeerName(false); + let producer_id = await roomList .get(socket.room_id) .produce(socket.id, producerTransportId, rtpParameters, kind); log.debug('Produce', { kind: kind, - peer_name: getPeerName(false), + peer_name: peer_name, producer_id: producer_id, }); + // peer_info audio Or video ON + let data = { + peer_name: peer_name, + type: kind, + status: true, + }; + roomList.get(socket.room_id).getPeers().get(socket.id).updatePeerInfo(data); + callback({ producer_id, }); @@ -323,10 +344,12 @@ io.on('connection', (socket) => { callback(params); }); - socket.on('producerClosed', ({ producer_id }) => { - log.debug('Producer close', getPeerName()); + socket.on('producerClosed', (data) => { + log.debug('Producer close', data); - roomList.get(socket.room_id).closeProducer(socket.id, producer_id); + // peer_info audio Or video OFF + roomList.get(socket.room_id).getPeers().get(socket.id).updatePeerInfo(data); + roomList.get(socket.room_id).closeProducer(socket.id, data.producer_id); }); socket.on('resume', async (_, callback) => {