fix: disable browser DSP on screen share audio and harden ICE stats resolution

Screen share audio was muffled/gated because getDisplayMedia used plain
`audio: true`, letting the browser apply voice-optimized DSP (NS, AEC, AGC)
to desktop audio. Now passes explicit constraints disabling all processing
and requesting stereo capture.

Also improves WebRTC stats: three-tier ICE candidate-pair discovery
(transport → active-bytes heuristic → legacy fallback), height-based
simulcast layer inference, and documents mDNS obfuscation limitation.
This commit is contained in:
Jannis Braun
2026-02-24 02:31:24 +01:00
parent f808a204e7
commit 2342396fce
2 changed files with 72 additions and 39 deletions
+59 -31
View File
@@ -32,6 +32,15 @@ export interface VideoTrackStat {
simulcastLayer: string | null; simulcastLayer: string | null;
} }
/**
* Network-level WebRTC stats for the active ICE transport.
*
* `serverAddress`, `protocol`, and `candidateType` may legitimately remain null
* on strict LANs where Chrome's mDNS IP obfuscation masks candidate-pair addresses.
* When ICE candidates use mDNS hostnames (e.g. "abcd-1234.local") instead of raw IPs,
* the browser's stats API returns the obfuscated hostname and we cannot resolve the
* underlying address. This is a known WebRTC platform limitation, not a bug in our code.
*/
export interface NetworkStats { export interface NetworkStats {
ping: number | null; ping: number | null;
packetLoss: number | null; packetLoss: number | null;
@@ -121,12 +130,15 @@ function discoverPeerConnections(room: any): RTCPeerConnection[] {
return pcs; return pcs;
} }
function inferSimulcastLayer(width: number | null): string | null { function inferSimulcastLayer(width: number | null, height: number | null): string | null {
if (width === null || width <= 0) return null; if (height !== null && height > 0) {
if (width >= 1920) return 'High'; if (height >= 1000) return 'High';
if (width >= 1280) return 'Medium'; if (height >= 700) return 'Medium';
return 'Low'; return 'Low';
} }
if (width !== null && width > 0) return 'Low';
return null;
}
// ── Hook ── // ── Hook ──
@@ -169,7 +181,6 @@ export function useTrackStats(enabled: boolean): TrackStatsSnapshot | null {
// Global codec map across all PCs // Global codec map across all PCs
const globalCodecMap = new Map<string, string>(); const globalCodecMap = new Map<string, string>();
let selectedCandidatePairRemoteId: string | null = null;
for (const pc of pcs) { for (const pc of pcs) {
let reports: RTCStatsReport; let reports: RTCStatsReport;
@@ -179,39 +190,55 @@ export function useTrackStats(enabled: boolean): TrackStatsSnapshot | null {
continue; continue;
} }
// Pass 1: Collect codecs, find transport's selected candidate pair
let selectedPairId: string | null = null;
reports.forEach((report: any) => { reports.forEach((report: any) => {
if (report.type === 'codec') { if (report.type === 'codec') {
globalCodecMap.set(report.id, report.mimeType?.split('/')[1] ?? report.mimeType ?? ''); globalCodecMap.set(report.id, report.mimeType?.split('/')[1] ?? report.mimeType ?? '');
} }
if (report.type === 'transport' && report.selectedCandidatePairId) {
if (report.type === 'candidate-pair' && report.state === 'succeeded') { selectedPairId = report.selectedCandidatePairId;
if (report.currentRoundTripTime != null) {
network.ping = Math.round(report.currentRoundTripTime * 1000);
}
if (!network.serverAddress && report.remoteCandidateId) {
selectedCandidatePairRemoteId = report.remoteCandidateId;
}
}
if (report.type === 'remote-candidate' && !network.serverAddress) {
const addr = report.address || report.ip;
if (addr) {
network.serverAddress = addr;
network.protocol = report.protocol ?? null;
network.candidateType = report.candidateType ?? null;
}
} }
}); });
// Safari fallback: look up remote candidate by ID // Pass 2: Resolve active candidate pair (three-tier fallback)
if (!network.serverAddress && selectedCandidatePairRemoteId) { let activePair: any = null;
let reports2: RTCStatsReport; if (selectedPairId) {
try { // Tier 1: Spec-correct — transport points directly to the active pair
reports2 = await pc.getStats(); activePair = reports.get(selectedPairId);
} catch {
continue;
} }
const remoteCandidate = reports2.get(selectedCandidatePairRemoteId); if (!activePair) {
// Tier 2: Active-bytes heuristic — the pair carrying the most data IS
// the active transport, regardless of state label or mDNS obfuscation
let maxBytes = 0;
reports.forEach((report: any) => {
if (report.type === 'candidate-pair') {
const total = (report.bytesSent ?? 0) + (report.bytesReceived ?? 0);
if (total > maxBytes) {
maxBytes = total;
activePair = report;
}
}
});
}
if (!activePair) {
// Tier 3: Legacy fallback — first pair with state succeeded or in-progress
reports.forEach((report: any) => {
if (report.type === 'candidate-pair' && !activePair) {
if (report.state === 'succeeded' || report.state === 'in-progress') {
activePair = report;
}
}
});
}
// Pass 3: Extract network info from active pair
if (activePair) {
if (activePair.currentRoundTripTime != null) {
network.ping = Math.round(activePair.currentRoundTripTime * 1000);
}
if (!network.serverAddress && activePair.remoteCandidateId) {
const remoteCandidate = reports.get(activePair.remoteCandidateId);
if (remoteCandidate) { if (remoteCandidate) {
const addr = remoteCandidate.address || remoteCandidate.ip; const addr = remoteCandidate.address || remoteCandidate.ip;
if (addr) { if (addr) {
@@ -222,6 +249,7 @@ export function useTrackStats(enabled: boolean): TrackStatsSnapshot | null {
} }
} }
} }
}
// ── Step B: Build TrackIdentityMap ── // ── Step B: Build TrackIdentityMap ──
const identityMap = new Map<string, TrackIdentity>(); const identityMap = new Map<string, TrackIdentity>();
@@ -502,7 +530,7 @@ export function useTrackStats(enabled: boolean): TrackStatsSnapshot | null {
height, height,
fps, fps,
qualityLimitation: null, qualityLimitation: null,
simulcastLayer: inferSimulcastLayer(width), simulcastLayer: inferSimulcastLayer(width, height),
}); });
} }
} }
+6 -1
View File
@@ -121,7 +121,12 @@ export async function startScreenShare(room: Room): Promise<boolean> {
try { try {
const track = await room.localParticipant.setScreenShareEnabled(true, { const track = await room.localParticipant.setScreenShareEnabled(true, {
audio: true, audio: {
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
channelCount: 2,
},
resolution: { width: opts.capture.width, height: opts.capture.height }, resolution: { width: opts.capture.width, height: opts.capture.height },
// @ts-ignore — LiveKit accepts frameRate at capture level // @ts-ignore — LiveKit accepts frameRate at capture level
frameRate: opts.capture.frameRate, frameRate: opts.capture.frameRate,