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" }); this.collectAllStats(); } 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", () => { 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, this.collectAndPostSingleStat); if (Object.keys(stats).length === 0 || !stats) return; window.postMessage( { event: "webrtc-internal-exporter:peer-connection-stats", stats }, stats ); } async collectAllStats() { const stats = []; for (const [id, pc] of this.peerConnections) { if (this.url && this.enabled) { const pcStats = await this.collectStats(id, pc); stats.push(pcStats); } } window.postMessage( { event: "webrtc-internal-exporter:peer-connections-stats", data: JSON.parse(JSON.stringify(stats)), }, ); log(`Stats collected:`, JSON.parse(JSON.stringify(stats))); setTimeout(this.collectAllStats.bind(this), this.updateInterval); return stats; } /** * @param {string} id * @param {RTCPeerConnection} pc * @param {Function} binding */ async collectStats(id, pc, binding) { var completeStats = {}; if (!pc) { pc = this.peerConnections.get(id); if (!pc) return; } 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); } else { if (binding) { setTimeout(binding.bind(this), this.updateInterval, 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; }, });