function log(...args) { console.log.apply(null, ["[webrtc-internal-exporter:override]", ...args]); } log("Override RTCPeerConnection."); class WebrtcInternalExporter { peerConnections = new Map(); url = ""; enabled = false; updateInterval = 2000; enabledStats = []; constructor() { window.addEventListener("message", async (message) => { const { event, options } = message.data; if (event === "webrtc-internal-exporter:options") { log("options updated:", options); Object.assign(this, options); } }); window.postMessage({ event: "webrtc-internal-exporter:ready" }); setInterval(() => this.collectAndPostAllStats(), this.updateInterval); } randomId() { return ( window.crypto?.randomUUID() || (2 ** 64 * Math.random()).toString(16) ); } /** * @param {RTCPeerConnection} pc */ add(pc) { const id = this.randomId(); pc.iceCandidates = []; pc.iceCandidateErrors = []; this.peerConnections.set(id, pc); pc.addEventListener("connectionstatechange", () => { log(`connectionStateChange: ${pc.connectionState}`); this.collectAndPostAllStats(); if (pc.connectionState === "closed") { this.peerConnections.delete(id); } }); /** * @param {RTCPeerConnectionIceErrorEvent} event */ pc.addEventListener("icecandidateerror", (event) => { this.peerConnections.get(id).iceCandidateErrors.push({ timestamp: Date.now(), address: event.errorAddress, errorCode: event.errorCode, errorText: event.errorText, port: event.errorPort, url: event.url, }); }); /** * @param {RTCPeerConnectionIceEvent} event */ pc.addEventListener("icecandidate", (event) => { this.peerConnections.get(id).iceCandidates.push({ timestamp: Date.now(), candidate: event.candidate?.candidate, component: event.candidate?.component, foundation: event.candidate?.foundation, port: event.candidate?.port, priority: event.candidate?.priority, protocol: event.candidate?.protocol, relatedAddress: event.candidate?.relatedAddress, relatedPort: event.candidate?.relatedPort, sdpMLineIndex: event.candidate?.sdpMLineIndex, sdpMid: event.candidate?.sdpMid, tcpType: event.candidate?.tcpType, type: event.candidate?.type, usernameFragment: event.candidate?.usernameFragment, }); }); } async collectAndPostSingleStat(id) { const stats = await this.collectStats(id); if (Object.keys(stats).length === 0 || !stats) return; window.postMessage( { event: "webrtc-internal-exporter:peer-connection-stats", stats: [stats] }, [stats] ); log(`Single stat collected:`, [stats]); } async collectAndPostAllStats() { const stats = []; for (const [id] of this.peerConnections) { if (this.url && this.enabled) { const pcStats = await this.collectStats(id); if (Object.keys(pcStats).length === 0 || !pcStats) continue; stats.push(pcStats); } } window.postMessage( { event: "webrtc-internal-exporter:peer-connections-stats", data: stats }, stats ); log(`Stats collected:`, stats); return stats; } /** * @param {string} id */ async collectStats(id) { var pc = this.peerConnections.get(id); if (!pc) return; var completeStats = {}; if (this.url && this.enabled) { try { const stats = await pc.getStats(); const values = [...stats.values()].filter( (v) => ["peer-connection", ...this.enabledStats].indexOf(v.type) !== -1 ); completeStats = { url: window.location.href, id, connectionState: pc.connectionState, iceConnectionState: pc.iceConnectionState, iceGatheringState: pc.iceGatheringState, signalingState: pc.signalingState, iceCandidateErrors: pc.iceCandidateErrors, iceCandidates: pc.iceCandidates, values, }; } catch (error) { log(`collectStats error: ${error.message}`); } } if (pc.connectionState === "closed") { this.peerConnections.delete(id); } return completeStats; } } const webrtcInternalExporter = new WebrtcInternalExporter(); window.RTCPeerConnection = new Proxy(window.RTCPeerConnection, { construct(target, argumentsList) { log(`RTCPeerConnection`, argumentsList); const pc = new target(...argumentsList); webrtcInternalExporter.add(pc); return pc; }, });