tune voice quality and monitor media health
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 33s

This commit is contained in:
mats committed 2026-09-22 17:03:01 +02:00
1 parent cb793c7ab6
commit f2d62ac142
2 files changed
+97 -2

No files matched your search

+93 -2
View File
@@ -131,6 +131,78 @@ async function selectedIcePath(pc) {
};
}
async function tuneAudioSender(sender) {
if (!sender?.getParameters || !sender?.setParameters) return;
const params = sender.getParameters();
if (!params.encodings?.length) params.encodings = [{}];
params.encodings[0].maxBitrate = 64000;
params.encodings[0].dtx = false;
params.encodings[0].priority = 'high';
await sender.setParameters(params);
}
function preferOpus(pc) {
const capabilities = globalThis.RTCRtpReceiver?.getCapabilities?.('audio');
const codecs = capabilities?.codecs || [];
const opus = codecs.filter((codec) => codec.mimeType?.toLowerCase() === 'audio/opus');
if (!opus.length) return;
const ordered = [...opus, ...codecs.filter((codec) => !opus.includes(codec))];
for (const transceiver of pc.getTransceivers()) {
if (transceiver.receiver?.track?.kind === 'audio' || transceiver.sender?.track?.kind === 'audio') {
try { transceiver.setCodecPreferences(ordered); } catch (err) {
captureRumIssue('Could not prefer Opus audio codec', 'WARNING', { error: err?.message || String(err) });
}
}
}
}
function monitorPeerQuality(peer) {
if (peer.qualityTimer) clearInterval(peer.qualityTimer);
peer.qualityTimer = setInterval(async () => {
const pc = peer.pc;
if (!pc || pc.connectionState !== 'connected' || !pc.getStats) return;
try {
const stats = await pc.getStats();
let inbound;
let outbound;
let remoteInbound;
stats.forEach((report) => {
if (report.kind !== 'audio' && report.mediaType !== 'audio') return;
if (report.type === 'inbound-rtp') inbound = report;
if (report.type === 'outbound-rtp') outbound = report;
if (report.type === 'remote-inbound-rtp') remoteInbound = report;
});
const packets = (inbound?.packetsReceived || 0) + (inbound?.packetsLost || 0);
const lossRatio = packets > 20 ? (inbound.packetsLost || 0) / packets : 0;
const jitterMs = (inbound?.jitter || 0) * 1000;
const rttMs = (remoteInbound?.roundTripTime || 0) * 1000;
const issue = lossRatio >= 0.05 || jitterMs >= 80 || rttMs >= 500;
if (issue && (!peer.lastQualityNotice || Date.now() - peer.lastQualityNotice > ICE_NOTICE_INTERVAL)) {
peer.lastQualityNotice = Date.now();
const details = `quality degraded (loss ${Math.round(lossRatio * 100)}%, jitter ${Math.round(jitterMs)}ms${rttMs ? `, RTT ${Math.round(rttMs)}ms` : ''})`;
toast(`${peer.name}: ${details}`, 'warn');
captureRumIssue('WebRTC audio quality degraded', 'WARNING', {
peerId: peer.id,
peerName: peer.name,
lossRatio,
jitterMs,
rttMs,
packetsReceived: inbound?.packetsReceived,
packetsLost: inbound?.packetsLost,
bytesReceived: inbound?.bytesReceived,
bytesSent: outbound?.bytesSent,
path: peer.icePath?.path,
});
}
} catch (err) {
captureRumIssue('Could not inspect WebRTC audio quality', 'WARNING', {
peerId: peer.id,
error: err?.message || String(err),
});
}
}, 5000);
}
function playRetroCue(name) {
const ctx = S.audioCtx;
if (!ctx) return;
@@ -191,7 +263,14 @@ function effectiveIce() {
}
function microphoneConstraints(deviceId = localStorage.getItem(LS_AUDIO_INPUT)) {
const audio = { echoCancellation: true, noiseSuppression: true, autoGainControl: true };
const audio = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
channelCount: 1,
sampleRate: 48000,
sampleSize: 16,
};
if (deviceId) audio.deviceId = { exact: deviceId };
return { audio };
}
@@ -561,6 +640,8 @@ function makePeerRecord(p) {
resetAttempts: 0,
lastIceNotice: null,
icePath: null,
qualityTimer: null,
lastQualityNotice: 0,
els: {},
};
}
@@ -658,16 +739,24 @@ function ensurePC(peer, initiator) {
error: err?.message || String(err),
});
});
monitorPeerQuality(peer);
}
};
pc.ontrack = (ev) => onRemoteTrack(peer, ev);
// send my mic; if I have none, ask to receive theirs
if (S.micStream) {
for (const track of S.micStream.getAudioTracks()) pc.addTrack(track, S.micStream);
for (const track of S.micStream.getAudioTracks()) {
const sender = pc.addTrack(track, S.micStream);
tuneAudioSender(sender).catch((err) => captureRumIssue('Could not tune Opus audio sender', 'WARNING', {
peerId: peer.id,
error: err?.message || String(err),
}));
}
} else {
pc.addTransceiver('audio', { direction: 'recvonly' });
}
preferOpus(pc);
// send my screen share if I am currently sharing when this peer connects
if (S.sharing) {
@@ -710,6 +799,7 @@ function recreatePeerConnection(peer, { announce }) {
send({ type: 'reset', to: peer.id });
}
if (peer.pc) { try { peer.pc.close(); } catch {} }
if (peer.qualityTimer) { clearInterval(peer.qualityTimer); peer.qualityTimer = null; }
peer.pc = null;
peer.makingOffer = false;
peer.ignoreOffer = false;
@@ -754,6 +844,7 @@ async function onCandidate(peer, candidate) {
function destroyPeer(peer) {
if (peer.pc) { try { peer.pc.close(); } catch {} peer.pc = null; }
if (peer.qualityTimer) { clearInterval(peer.qualityTimer); peer.qualityTimer = null; }
detachSpatial(peer);
if (peer.audioEl) {
try { peer.audioEl.pause(); peer.audioEl.srcObject = null; } catch {}