fix volume and quality
This commit is contained in:
1 parent
143dd26c31
commit
10481ad08c
1 file changed
+40
-12
+40
-12
@@ -139,7 +139,10 @@ async function tuneAudioSender(sender) {
|
|||||||
if (!sender?.getParameters || !sender?.setParameters) return;
|
if (!sender?.getParameters || !sender?.setParameters) return;
|
||||||
const params = sender.getParameters();
|
const params = sender.getParameters();
|
||||||
if (!params.encodings?.length) params.encodings = [{}];
|
if (!params.encodings?.length) params.encodings = [{}];
|
||||||
params.encodings[0].maxBitrate = 64000;
|
// 128 kbps keeps Opus in its fullband LD/MD modes with headroom for FEC;
|
||||||
|
// 64 kbps audibly compresses louder/denser speech. DTX stays off so the
|
||||||
|
// channel doesn't pump in and out of low-bitrate comfort noise.
|
||||||
|
params.encodings[0].maxBitrate = 128000;
|
||||||
params.encodings[0].dtx = false;
|
params.encodings[0].dtx = false;
|
||||||
params.encodings[0].priority = 'high';
|
params.encodings[0].priority = 'high';
|
||||||
await sender.setParameters(params);
|
await sender.setParameters(params);
|
||||||
@@ -1073,17 +1076,19 @@ function stopScreenShare(stream, silent) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// position sync
|
// position sync
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Fans new joiners out from directly in front of the room's center: the
|
// First joiner takes the room's middle anchor; later joiners fan out around
|
||||||
// first joiner lands straight ahead, then each next one alternates sides,
|
// them — the 2nd participant lands straight ahead (0deg), then each next one
|
||||||
// widening by 10deg per pair (0, +10, -10, +20, -20, ...) instead of the
|
// alternates sides, widening by 10deg per pair (0, +10, -10, +20, -20, ...).
|
||||||
// old distance-maximizing grid search, which tended to dump joiners in a
|
// Chosen once per client at join and broadcast via the signaling 'pos'
|
||||||
// far corner rather than somewhere natural to face the group.
|
// message, so every client converges on the same layout.
|
||||||
function chooseOpenParticipantPosition() {
|
function chooseOpenParticipantPosition() {
|
||||||
const ANCHOR = { x: 50, y: 50 };
|
const ANCHOR = { x: 50, y: 50 };
|
||||||
const RADIUS = 24;
|
const RADIUS = 24;
|
||||||
const n = S.peers.size;
|
const n = S.peers.size;
|
||||||
const step = Math.ceil(n / 2) * 10;
|
if (n === 0) return { x: ANCHOR.x, y: ANCHOR.y }; // first joiner sits in the middle
|
||||||
const angleDeg = n === 0 ? 0 : (n % 2 === 1 ? step : -step);
|
const k = n - 1; // fan slot: 0deg, +10, -10, +20, -20...
|
||||||
|
const mag = Math.ceil(k / 2) * 10;
|
||||||
|
const angleDeg = k === 0 ? 0 : (k % 2 === 1 ? mag : -mag);
|
||||||
const rad = (angleDeg * Math.PI) / 180;
|
const rad = (angleDeg * Math.PI) / 180;
|
||||||
return {
|
return {
|
||||||
x: ANCHOR.x + RADIUS * Math.sin(rad),
|
x: ANCHOR.x + RADIUS * Math.sin(rad),
|
||||||
@@ -1123,18 +1128,24 @@ function updateMicSignal(level = S.audioManager.getLevel()) {
|
|||||||
if (clipping) clipping.classList.toggle('hidden', !level.clipping);
|
if (clipping) clipping.classList.toggle('hidden', !level.clipping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let meRosterEl = null; // roster row for "me", kept so speaking highlights don't need a rebuild
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!S.audioCtx) return;
|
if (!S.audioCtx) return;
|
||||||
const localLevel = S.audioManager.getLevel();
|
const localLevel = S.audioManager.getLevel();
|
||||||
updateMicSignal(localLevel);
|
updateMicSignal(localLevel);
|
||||||
const meSpeaking = S.micStream && !S.muted && localLevel.rms > 0.04;
|
const meSpeaking = S.micStream && !S.muted && localLevel.rms > 0.04;
|
||||||
els.meAvatar.classList.toggle('speaking', !!meSpeaking);
|
els.meAvatar.classList.toggle('speaking', !!meSpeaking);
|
||||||
let changed = false;
|
// Toggle speaking highlights in place. A full renderTiles() here used to
|
||||||
|
// wipe and rebuild the roster DOM on every start/stop of speech — which
|
||||||
|
// destroyed the volume popup and its range input mid-drag, making the
|
||||||
|
// per-user volume control feel broken exactly while a peer was talking.
|
||||||
|
meRosterEl?.classList.toggle('speaking', !!meSpeaking);
|
||||||
for (const peer of S.peers.values()) {
|
for (const peer of S.peers.values()) {
|
||||||
const speaking = !!(peer.analyser && !S.deafened && rms(peer.analyser) > 0.03);
|
const speaking = !!(peer.analyser && !S.deafened && rms(peer.analyser) > 0.03);
|
||||||
if (speaking !== peer.speaking) { peer.speaking = speaking; changed = true; }
|
if (speaking === peer.speaking) continue;
|
||||||
|
peer.speaking = speaking;
|
||||||
|
peer.els.rosterItem?.classList.toggle('speaking', speaking);
|
||||||
}
|
}
|
||||||
if (changed) renderTiles();
|
|
||||||
updateScreenTileIndicators();
|
updateScreenTileIndicators();
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
@@ -1170,8 +1181,13 @@ function renderChannelCounts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTiles() {
|
function renderTiles() {
|
||||||
|
// Remember an open volume popup so it survives this rebuild (peer join/leave
|
||||||
|
// or a mute/share badge change) without snapping closed under the cursor.
|
||||||
|
const openVolPeerId = document.querySelector('.channel-user-item.show-vol[data-peer-id]')?.dataset.peerId || null;
|
||||||
|
|
||||||
// Clear any existing user rosters in voice channels
|
// Clear any existing user rosters in voice channels
|
||||||
document.querySelectorAll('.channel-users').forEach((el) => { el.innerHTML = ''; });
|
document.querySelectorAll('.channel-users').forEach((el) => { el.innerHTML = ''; });
|
||||||
|
meRosterEl = null;
|
||||||
|
|
||||||
// For each voice channel element in the sidebar
|
// For each voice channel element in the sidebar
|
||||||
document.querySelectorAll('.channel.voice').forEach((channelEl) => {
|
document.querySelectorAll('.channel.voice').forEach((channelEl) => {
|
||||||
@@ -1189,6 +1205,7 @@ function renderTiles() {
|
|||||||
// Current active room: render Me + live peers (with interactive volume sliders)
|
// Current active room: render Me + live peers (with interactive volume sliders)
|
||||||
const meItem = document.createElement('div');
|
const meItem = document.createElement('div');
|
||||||
meItem.className = 'channel-user-item' + (els.meAvatar.classList.contains('speaking') ? ' speaking' : '');
|
meItem.className = 'channel-user-item' + (els.meAvatar.classList.contains('speaking') ? ' speaking' : '');
|
||||||
|
meRosterEl = meItem;
|
||||||
const meAv = document.createElement('div');
|
const meAv = document.createElement('div');
|
||||||
meAv.className = 'avatar user-line-avatar';
|
meAv.className = 'avatar user-line-avatar';
|
||||||
setAvatar(meAv, S.name, S.myId || 'me');
|
setAvatar(meAv, S.name, S.myId || 'me');
|
||||||
@@ -1226,6 +1243,7 @@ function renderTiles() {
|
|||||||
for (const peer of S.peers.values()) {
|
for (const peer of S.peers.values()) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'channel-user-item' + (peer.speaking ? ' speaking' : '');
|
item.className = 'channel-user-item' + (peer.speaking ? ' speaking' : '');
|
||||||
|
item.dataset.peerId = peer.id;
|
||||||
|
|
||||||
const av = document.createElement('div');
|
const av = document.createElement('div');
|
||||||
av.className = 'avatar user-line-avatar';
|
av.className = 'avatar user-line-avatar';
|
||||||
@@ -1271,7 +1289,8 @@ function renderTiles() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
peer.vol = Number(volInput.value);
|
peer.vol = Number(volInput.value);
|
||||||
volLabel.textContent = `User Volume: ${Math.round(peer.vol * 100)}%`;
|
volLabel.textContent = `User Volume: ${Math.round(peer.vol * 100)}%`;
|
||||||
if (peer.gain && !S.deafened) peer.gain.gain.value = peer.vol;
|
// setTargetAtTime ramps smoothly instead of zipper-clicking on drag
|
||||||
|
if (peer.gain && S.audioCtx && !S.deafened) peer.gain.gain.setTargetAtTime(peer.vol, S.audioCtx.currentTime, 0.02);
|
||||||
};
|
};
|
||||||
volPop.append(volLabel, volInput);
|
volPop.append(volLabel, volInput);
|
||||||
|
|
||||||
@@ -1283,6 +1302,7 @@ function renderTiles() {
|
|||||||
|
|
||||||
item.append(av, name, badges, volPop);
|
item.append(av, name, badges, volPop);
|
||||||
roster.appendChild(item);
|
roster.appendChild(item);
|
||||||
|
peer.els.rosterItem = item;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Remote / unjoined channel: render users from server roster snapshot
|
// Remote / unjoined channel: render users from server roster snapshot
|
||||||
@@ -1324,6 +1344,14 @@ function renderTiles() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Re-open the volume popup on the rebuilt row for the peer whose popup was
|
||||||
|
// open before this structural rebuild.
|
||||||
|
if (openVolPeerId) {
|
||||||
|
document
|
||||||
|
.querySelector(`.channel-user-item[data-peer-id="${CSS.escape(openVolPeerId)}"]`)
|
||||||
|
?.classList.add('show-vol');
|
||||||
|
}
|
||||||
|
|
||||||
renderStageEmpty();
|
renderStageEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user