From 2e2a69e2be84ec865857178334493633ad2aed76 Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Sun, 3 Nov 2024 12:32:25 +0100 Subject: [PATCH] [mirotalksfu] - #171 add complete volume control --- app/src/Server.js | 2 +- app/src/config.template.js | 5 +- package.json | 2 +- public/js/Room.js | 4 +- public/js/RoomClient.js | 188 ++++++++++++++++++++++++++++++------- public/js/Rules.js | 5 +- 6 files changed, 165 insertions(+), 41 deletions(-) diff --git a/app/src/Server.js b/app/src/Server.js index ea97916e..ef37b435 100644 --- a/app/src/Server.js +++ b/app/src/Server.js @@ -55,7 +55,7 @@ dev dependencies: { * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.6.18 + * @version 1.6.19 * */ diff --git a/app/src/config.template.js b/app/src/config.template.js index 8154b28c..465ddc07 100644 --- a/app/src/config.template.js +++ b/app/src/config.template.js @@ -454,6 +454,7 @@ module.exports = { snapShotButton: true, muteAudioButton: true, videoPrivacyButton: true, + audioVolumeInput: true, }, consumerVideo: { videoPictureInPicture: true, @@ -466,7 +467,7 @@ module.exports = { sendVideoButton: true, muteVideoButton: true, muteAudioButton: true, - audioVolumeInput: true, // Disabled for mobile + audioVolumeInput: true, geolocationButton: true, // Presenter banButton: true, // presenter ejectButton: true, // presenter @@ -476,7 +477,7 @@ module.exports = { sendFileButton: true, sendVideoButton: true, muteAudioButton: true, - audioVolumeInput: true, // Disabled for mobile + audioVolumeInput: true, geolocationButton: true, // Presenter banButton: true, // presenter ejectButton: true, // presenter diff --git a/package.json b/package.json index 0bdc0049..f88136e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalksfu", - "version": "1.6.18", + "version": "1.6.19", "description": "WebRTC SFU browser-based video calls", "main": "Server.js", "scripts": { diff --git a/public/js/Room.js b/public/js/Room.js index 65d87be7..12d089ca 100644 --- a/public/js/Room.js +++ b/public/js/Room.js @@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.6.18 + * @version 1.6.19 * */ @@ -4563,7 +4563,7 @@ function showAbout() { imageUrl: image.about, customClass: { image: 'img-about' }, position: 'center', - title: 'WebRTC SFU v1.6.18', + title: 'WebRTC SFU v1.6.19', html: `
diff --git a/public/js/RoomClient.js b/public/js/RoomClient.js index 5cb98066..66bd4eaa 100644 --- a/public/js/RoomClient.js +++ b/public/js/RoomClient.js @@ -1404,6 +1404,9 @@ class RoomClient { throw new Error('Producer not found!'); } + console.log('PRODUCER MEDIA TYPE ----> ' + type); + console.log('PRODUCER', producer); + this.producers.set(producer.id, producer); this.producerLabel.set(type, producer.id); @@ -1812,6 +1815,23 @@ class RoomClient { return button; } + getConsumerIdByProducerId(producerId) { + for (let [consumerId, consumer] of this.consumers.entries()) { + if (consumer._producerId === producerId) { + return consumerId; + } + } + return null; + } + + getProducerIdByConsumerId(consumerId) { + const consumer = this.consumers.get(consumerId); + if (consumer) { + return consumer._producerId; + } + return null; + } + // #################################################### // PRODUCER // #################################################### @@ -1853,7 +1873,7 @@ class RoomClient { } async handleProducer(id, type, stream) { - let elem, vb, vp, ts, d, p, i, au, pip, fs, pm, pb, pn, mv; + let elem, vb, vp, ts, d, p, i, au, pip, fs, pm, pb, pn, pv, mv; switch (type) { case mediaType.video: case mediaType.screen: @@ -1902,6 +1922,15 @@ class RoomClient { pb.className = 'bar'; pb.style.height = '1%'; pm.appendChild(pb); + + pv = document.createElement('input'); + pv.id = this.peer_id + '___pVolume'; + pv.type = 'range'; + pv.min = 0; + pv.max = 100; + pv.value = 100; + + BUTTONS.producerVideo.audioVolumeInput && vb.appendChild(pv); BUTTONS.producerVideo.muteAudioButton && vb.appendChild(au); BUTTONS.producerVideo.videoPrivacyButton && !isScreen && vb.appendChild(vp); BUTTONS.producerVideo.snapShotButton && vb.appendChild(ts); @@ -1931,6 +1960,7 @@ class RoomClient { this.handleMV(elem.id, mv.id); this.handlePN(elem.id, pn.id, d.id, isScreen); this.handleZV(elem.id, d.id, this.peer_id); + this.handlePV(id + '___' + pv.id); if (!isScreen) this.handleVP(elem.id, vp.id); @@ -1962,6 +1992,7 @@ class RoomClient { this.myAudioEl = elem; this.localAudioEl.appendChild(elem); await this.attachMediaStream(elem, stream, type, 'Producer'); + this.handlePV(elem.id + '___' + this.peer_id + '___pVolume'); console.log('[addProducer] audio-element-count', this.localAudioEl.childElementCount); break; default: @@ -2380,7 +2411,7 @@ class RoomClient { this.handleSV(sv.id); BUTTONS.consumerVideo.muteVideoButton && this.handleCM(cm.id); BUTTONS.consumerVideo.muteAudioButton && this.handleAU(au.id); - this.handlePV(id + '___' + pv.id); + this.handleCV(id + '___' + pv.id); this.handleGL(gl.id); this.handleBAN(ban.id); this.handleKO(ko.id); @@ -2440,7 +2471,7 @@ class RoomClient { let inputPv = this.getId(audioConsumerId); if (inputPv) { - this.handlePV(id + '___' + audioConsumerId); + this.handleCV(id + '___' + audioConsumerId); this.setPeerAudio(remotePeerId, remotePeerAudio); } @@ -2539,12 +2570,14 @@ class RoomClient { au = this.createButton(peer_id + '__audio', peer_audio ? html.audioOn : html.audioOff); if (remotePeer) { + // pv = document.createElement('input'); pv.id = peer_id + '___pVolume'; pv.type = 'range'; pv.min = 0; pv.max = 100; pv.value = 100; + sf = this.createButton('remotePeer___' + peer_id + '___sendFile', html.sendFile); sm = this.createButton('remotePeer___' + peer_id + '___sendMsg', html.sendMsg); sv = this.createButton('remotePeer___' + peer_id + '___sendVideo', html.sendVideo); @@ -2579,8 +2612,9 @@ class RoomClient { BUTTONS.videoOff.sendVideoButton && vb.appendChild(sv); BUTTONS.videoOff.sendFileButton && vb.appendChild(sf); BUTTONS.videoOff.sendMessageButton && vb.appendChild(sm); - BUTTONS.videoOff.audioVolumeInput && !this.isMobileDevice && vb.appendChild(pv); + BUTTONS.videoOff.audioVolumeInput && vb.appendChild(pv); } + vb.appendChild(au); d.appendChild(i); d.appendChild(p); @@ -2592,7 +2626,7 @@ class RoomClient { BUTTONS.videoOff.muteAudioButton && this.handleAU(au.id); if (remotePeer) { - this.handlePV('remotePeer___' + pv.id); + this.handleCV(peer_id + '___' + pv.id); this.handleSM(sm.id); this.handleSF(sf.id); this.handleSV(sv.id); @@ -6840,44 +6874,129 @@ class RoomClient { } // #################################################### - // HANDLE PEER VOLUME - // ################################################### + // HANDLE PEERS AUDIO VOLUME + // #################################################### + + handleCV(uid) { + this.handleVolumeControl(uid, true); + } handlePV(uid) { + this.handleVolumeControl(uid, false); + } + + handleVolumeControl(uid, isConsumer = true) { const words = uid.split('___'); - let peer_id = words[1] + '___pVolume'; - let audioConsumerId = this.audioConsumers.get(peer_id); - let audioConsumerPlayer = this.getId(audioConsumerId); - let inputPv = this.getId(peer_id); - if (inputPv && audioConsumerPlayer) { - inputPv.style.display = 'inline'; - inputPv.value = 100; + const peer_id = `${words[1]}___pVolume`; + const audioPlayer = this.getId(isConsumer ? this.audioConsumers.get(peer_id) : words[0]); + const inputElement = this.getId(peer_id); + + if (inputElement && audioPlayer) { + inputElement.style.display = 'inline'; + inputElement.value = 100; + + let volumeUpdateTimeout; + const updateVolume = () => { - const volume = inputPv.value / 100; - this.setAudioVolume(audioConsumerPlayer, volume); + const volume = inputElement.value / 100; + this.setAudioVolume(audioPlayer, volume); + + // Clear any existing timeout to prevent sending too frequently + if (volumeUpdateTimeout) { + clearTimeout(volumeUpdateTimeout); + } + + // Set a timeout to send the update after 0.5 second + volumeUpdateTimeout = setTimeout(() => { + // Prepare the command to update peer volume + const cmd = { + type: 'peerAudio', + peer_name: this.peer_name, + [isConsumer ? 'audioConsumerId' : 'audioProducerId']: isConsumer + ? this.audioConsumers.get(peer_id) + : this.audioProducerId, + peer_id: peer_id, + volume: volume, + broadcast: true, + }; + this.emitCmd(cmd); + }, 500); // 0.5 second delay }; - inputPv.addEventListener('input', updateVolume); - inputPv.addEventListener('change', updateVolume); + + this.addVolumeEventListeners(inputElement, updateVolume); + } + } + + setAudioVolume(audioPlayer, volume) { + if (audioPlayer) { if (this.isMobileDevice) { - inputPv.addEventListener('touchstart', updateVolume); - inputPv.addEventListener('touchmove', updateVolume); + audioPlayer.muted = volume === 0; + if (!audioPlayer.muted) { + // Adjust playback rate as volume on mobile devices + audioPlayer.playbackRate = Math.max(0.1, volume); + } + } else { + // Set volume directly on desktop devices + audioPlayer.volume = volume; } } } - setAudioVolume(audioConsumerPlayer, volume) { - if (audioConsumerPlayer) { - if (this.isMobileDevice) { - // On mobile, we'll use a different approach - audioConsumerPlayer.muted = volume === 0; - if (!audioConsumerPlayer.muted) { - // We can only set volume to 1 on mobile, so we'll adjust playback rate instead - audioConsumerPlayer.playbackRate = Math.max(0.1, volume); - } - } else { - // On desktop, we can directly set the volume - audioConsumerPlayer.volume = volume; - } + handlePeerAudio(cmd) { + console.log('handlePeerAudio', { cmd }); + + const { peer_id, audioProducerId, audioConsumerId, volume } = cmd; + + const inputPV = this.getId(peer_id); + + if (!inputPV) return; + + inputPV.value = volume * 100; + + if (audioProducerId) { + this.handleConsumerAudio(audioProducerId, volume); + /* + If the producer has changed the volume from the default value of 100, + disable the volume input control on the consumer side to prevent further adjustments. + Otherwise, keep the input enabled if the volume is still at 100. + */ + inputPV.disabled = inputPV.value < 100 ? true : false; + } + + if (audioConsumerId) this.handleProducerAudio(audioConsumerId, volume); + } + + handleConsumerAudio(audioProducerId, volume) { + const consumerAudioId = this.getConsumerIdByProducerId(audioProducerId); + if (!consumerAudioId) return; + + const consumerAudioPlayer = this.getId(consumerAudioId); + if (!consumerAudioPlayer) return; + + this.setAudioVolume(consumerAudioPlayer, volume); + + console.log('handleConsumerPeerAudio', { consumerAudioId, consumerAudioPlayer }); + } + + handleProducerAudio(audioConsumerId, volume) { + const producerAudioId = this.getProducerIdByConsumerId(audioConsumerId); + if (!producerAudioId) return; + + const producerAudioPlayer = this.getId(producerAudioId); + if (!producerAudioPlayer) return; + + this.setAudioVolume(producerAudioPlayer, volume); + + console.log('handleProducerPeerAudio', { producerAudioId, producerAudioPlayer }); + } + + addVolumeEventListeners(inputElement, updateVolumeCallback) { + inputElement.addEventListener('input', updateVolumeCallback); + inputElement.addEventListener('change', updateVolumeCallback); + + if (this.isMobileDevice) { + inputElement.addEventListener('touchstart', updateVolumeCallback); + inputElement.addEventListener('touchmove', updateVolumeCallback); } } @@ -7060,6 +7179,9 @@ class RoomClient { case 'ejectAll': this.exit(); break; + case 'peerAudio': + this.handlePeerAudio(cmd); + break; default: break; //... diff --git a/public/js/Rules.js b/public/js/Rules.js index 3bb19a41..43264f9a 100644 --- a/public/js/Rules.js +++ b/public/js/Rules.js @@ -53,6 +53,7 @@ let BUTTONS = { snapShotButton: true, muteAudioButton: true, videoPrivacyButton: true, + audioVolumeInput: true, }, consumerVideo: { videoPictureInPicture: true, @@ -65,7 +66,7 @@ let BUTTONS = { sendVideoButton: true, muteVideoButton: true, muteAudioButton: true, - audioVolumeInput: true, // Disabled for mobile + audioVolumeInput: true, geolocationButton: true, // Presenter banButton: true, // presenter ejectButton: true, // presenter @@ -75,7 +76,7 @@ let BUTTONS = { sendFileButton: true, sendVideoButton: true, muteAudioButton: true, - audioVolumeInput: true, // Disabled for mobile + audioVolumeInput: true, geolocationButton: true, // Presenter banButton: true, // presenter ejectButton: true, // presenter