diff --git a/assets/js/voice.js b/assets/js/voice.js index 9426a55..196db8b 100644 --- a/assets/js/voice.js +++ b/assets/js/voice.js @@ -52,6 +52,8 @@ class VoiceChannel { this.voxHoldTime = 400; this.speakingUsers = new Set(); + this.remoteGainNodes = {}; // userId -> GainNode + this.userGains = JSON.parse(localStorage.getItem("voice_user_gains") || "{}"); this.setupPTTListeners(); this.loadWhisperSettings(); @@ -295,24 +297,44 @@ class VoiceChannel { pc.ontrack = (event) => { const stream = event.streams[0] || new MediaStream([event.track]); - if (this.audioContext && this.audioContext.state === 'suspended') this.audioContext.resume(); + if (!this.audioContext) { + this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: 48000, + latencyHint: "interactive" + }); + } + if (this.audioContext.state === "suspended") this.audioContext.resume(); + if (this.remoteAudios[userId]) { this.remoteAudios[userId].pause(); this.remoteAudios[userId].srcObject = null; this.remoteAudios[userId].remove(); } + + const source = this.audioContext.createMediaStreamSource(stream); + const gainNode = this.audioContext.createGain(); + const destination = this.audioContext.createMediaStreamDestination(); + + // Initialiser le gain de l utilisateur + const userGain = this.userGains[userId] !== undefined ? parseFloat(this.userGains[userId]) : 1.0; + gainNode.gain.value = userGain; + + source.connect(gainNode); + gainNode.connect(destination); + this.remoteGainNodes[userId] = gainNode; + const remoteAudio = new Audio(); remoteAudio.autoplay = true; - remoteAudio.style.display = 'none'; - remoteAudio.srcObject = stream; + remoteAudio.style.display = "none"; + remoteAudio.srcObject = destination.stream; remoteAudio.muted = this.isDeafened; remoteAudio.volume = this.settings.outputVolume || 1.0; - if (this.settings.outputDevice !== 'default' && typeof remoteAudio.setSinkId === 'function') { + if (this.settings.outputDevice !== "default" && typeof remoteAudio.setSinkId === "function") { remoteAudio.setSinkId(this.settings.outputDevice); } document.body.appendChild(remoteAudio); this.remoteAudios[userId] = remoteAudio; - remoteAudio.play().catch(e => console.warn('Autoplay prevented:', userId, e)); + remoteAudio.play().catch(e => console.warn("Autoplay prevented:", userId, e)); }; if (isOfferor && pc.signalingState === 'stable') pc.onnegotiationneeded(); @@ -529,6 +551,16 @@ class VoiceChannel { this.settings.outputVolume = parseFloat(vol); Object.values(this.remoteAudios).forEach(audio => { audio.volume = this.settings.outputVolume; }); } + + setUserGain(userId, volume) { + userId = String(userId); + this.userGains[userId] = parseFloat(volume); + localStorage.setItem("voice_user_gains", JSON.stringify(this.userGains)); + if (this.remoteGainNodes[userId]) { + this.remoteGainNodes[userId].gain.setTargetAtTime(this.userGains[userId], this.audioContext.currentTime, 0.01); + } + } + setInputVolume(vol) { this.settings.inputVolume = parseFloat(vol); if (this.inputGainNode && this.audioContext && !this.isSelfMuted) { @@ -606,7 +638,7 @@ class VoiceChannel { if (this.microphone) { try { this.microphone.disconnect(); } catch(e) {} } Object.values(this.peers).forEach(pc => pc.close()); Object.values(this.remoteAudios).forEach(audio => { audio.pause(); audio.remove(); audio.srcObject = null; }); - this.peers = {}; this.remoteAudios = {}; this.participants = {}; this.currentChannelId = null; this.myPeerId = null; this.speakingUsers.clear(); + this.peers = {}; this.remoteAudios = {}; this.remoteGainNodes = {}; this.participants = {}; this.currentChannelId = null; this.myPeerId = null; this.speakingUsers.clear(); document.querySelectorAll('.voice-item').forEach(el => el.classList.remove('active')); this.updateVoiceUI(); } @@ -691,6 +723,8 @@ class VoiceChannel { userEl.className = 'voice-user small text-muted d-flex align-items-center mb-1'; userEl.dataset.userId = userId; userEl.style.paddingLeft = '8px'; + userEl.style.cursor = 'pointer'; + userEl.title = 'Click to adjust volume'; const avatarStyle = avatarUrl ? `background-image: url('${avatarUrl}'); background-size: cover;` : "background-color: #555;"; const boxShadow = isSpeaking ? 'box-shadow: 0 0 0 2px #23a559;' : ''; let icons = ''; @@ -702,4 +736,66 @@ class VoiceChannel { ${icons}`; container.appendChild(userEl); } -} \ No newline at end of file +} + + +// Individual Gain UI Logic +document.addEventListener('click', (e) => { + const voiceUser = e.target.closest('.voice-user'); + if (!voiceUser) { + document.querySelectorAll('.voice-gain-popover').forEach(el => el.remove()); + return; + } + + const userId = voiceUser.dataset.userId; + if (userId === String(window.currentUserId)) return; + + e.stopPropagation(); + document.querySelectorAll('.voice-gain-popover').forEach(el => el.remove()); + + const rect = voiceUser.getBoundingClientRect(); + const popover = document.createElement('div'); + popover.className = 'voice-gain-popover p-2 rounded shadow border'; + popover.style.position = 'fixed'; + popover.style.left = (rect.right + 10) + 'px'; + popover.style.top = rect.top + 'px'; + popover.style.zIndex = '10000'; + popover.style.width = '180px'; + popover.style.backgroundColor = '#1e1f22'; + popover.style.borderColor = '#313338'; + popover.style.color = '#dbdee1'; + + const currentGain = (window.voiceHandler && window.voiceHandler.userGains[userId] !== undefined) ? window.voiceHandler.userGains[userId] : 1.0; + const percent = Math.round(currentGain * 100); + + popover.innerHTML = ` +