This commit is contained in:
parent
af46da77a7
commit
168ae4150e
4 changed files with 186 additions and 6 deletions
|
|
@ -27,12 +27,14 @@ const state = {
|
|||
deepFilterModule: null,
|
||||
viewMode: 'chat', // 'chat' or 'video'
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
peerGainNodes: new Map(), // userId -> GainNode
|
||||
},
|
||||
voicePresencePollId: null,
|
||||
chatWs: null,
|
||||
lastMessageId: null,
|
||||
onlineUsers: new Set(),
|
||||
idleUsers: new Set(),
|
||||
userVolumes: new Map(), // userId -> volume (0.0 to 2.0)
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -364,7 +366,48 @@ function renderChannels() {
|
|||
const pRow = document.createElement("div");
|
||||
pRow.className = `channel-row ${p.is_speaking ? 'voice-speaking' : ''}`;
|
||||
pRow.style.padding = "2px 8px";
|
||||
pRow.innerHTML = `<div class="avatar" style="width:20px;height:20px;font-size:10px">${shortName(p.display_name)}</div> <span>${escapeHtml(p.display_name)}</span>${p.is_muted ? '<i data-lucide="mic-off" class="voice-muted-icon"></i>' : ''}`;
|
||||
pRow.style.flexWrap = "wrap";
|
||||
|
||||
let sliderHtml = '';
|
||||
if (p.user_id !== state.me.id) {
|
||||
const vol = state.userVolumes.get(p.user_id) ?? 1.0;
|
||||
sliderHtml = `
|
||||
<div class="user-volume-control" style="width: 100%; display: flex; align-items: center; gap: 8px; padding-left: 28px; margin-top: 2px;">
|
||||
<i data-lucide="volume-2" style="width: 12px; height: 12px; opacity: 0.6;"></i>
|
||||
<input type="range" min="0" max="2" step="0.1" value="${vol}"
|
||||
class="volume-slider"
|
||||
data-user-id="${p.user_id}"
|
||||
style="flex: 1; height: 4px; cursor: pointer;">
|
||||
<span class="vol-pct" style="font-size: 10px; opacity: 0.6; min-width: 25px;">${Math.round(vol * 100)}%</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
pRow.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px; width: 100%;">
|
||||
<div class="avatar" style="width:20px;height:20px;font-size:10px">${shortName(p.display_name)}</div>
|
||||
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(p.display_name)}</span>
|
||||
${p.is_muted ? '<i data-lucide="mic-off" class="voice-muted-icon"></i>' : ''}
|
||||
</div>
|
||||
${sliderHtml}
|
||||
`;
|
||||
|
||||
const slider = pRow.querySelector('.volume-slider');
|
||||
if (slider) {
|
||||
slider.addEventListener('input', (e) => {
|
||||
const val = parseFloat(e.target.value);
|
||||
state.userVolumes.set(p.user_id, val);
|
||||
pRow.querySelector('.vol-pct').textContent = `${Math.round(val * 100)}%`;
|
||||
|
||||
const gainNode = state.voice.peerGainNodes.get(p.user_id);
|
||||
if (gainNode) {
|
||||
gainNode.gain.setTargetAtTime(val, state.voice.audioContext.currentTime, 0.05);
|
||||
}
|
||||
});
|
||||
// Stop propagation to prevent joining channel again when clicking slider
|
||||
slider.addEventListener('click', (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
pList.appendChild(pRow);
|
||||
}
|
||||
el.voiceChannelList.appendChild(pList);
|
||||
|
|
@ -987,7 +1030,6 @@ function ensurePeerConnection(peerId) {
|
|||
document.body.appendChild(audio);
|
||||
}
|
||||
audio.muted = false;
|
||||
audio.volume = 1;
|
||||
audio.srcObject = new MediaStream([event.track]);
|
||||
const tryPlay = () => {
|
||||
const playPromise = audio.play();
|
||||
|
|
@ -1000,6 +1042,28 @@ function ensurePeerConnection(peerId) {
|
|||
audio.onloadedmetadata = tryPlay;
|
||||
audio.oncanplay = tryPlay;
|
||||
tryPlay();
|
||||
|
||||
// Web Audio Pipeline for Volume Boosting
|
||||
if (!state.voice.audioContext) {
|
||||
state.voice.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
const ctx = state.voice.audioContext;
|
||||
if (ctx.state === 'suspended') ctx.resume();
|
||||
|
||||
const sourceNode = ctx.createMediaStreamSource(audio.srcObject);
|
||||
const gainNode = ctx.createGain();
|
||||
|
||||
const currentVolume = state.userVolumes.get(peerId) ?? 1.0;
|
||||
gainNode.gain.setValueAtTime(currentVolume, ctx.currentTime);
|
||||
|
||||
sourceNode.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
|
||||
state.voice.peerGainNodes.set(peerId, gainNode);
|
||||
|
||||
// Mute the original element as we play through Web Audio destination
|
||||
audio.volume = 0;
|
||||
audio.muted = true;
|
||||
} else if (event.track.kind === "video") {
|
||||
const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" };
|
||||
renderVideo(peerId, peer.display_name, stream, event.track.id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue