<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebRTC vs WebSocket Benchmark</title> <script src="https://cdn.jsdelivr.net/npm/alawmulaw"></script> <style> body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; } .container { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; max-width: 1400px; margin: 0 auto; } .panel { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .chat-container { height: 400px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin-bottom: 15px; } .message { margin-bottom: 10px; padding: 8px 12px; border-radius: 8px; max-width: 80%; } .message.user { background-color: #e3f2fd; margin-left: auto; } .message.assistant { background-color: #f5f5f5; } .metrics { margin-top: 15px; padding: 10px; background: #f8f9fa; border-radius: 8px; } .metric { margin: 5px 0; font-size: 14px; } button { background-color: #1976d2; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; } button:hover { background-color: #1565c0; } button:disabled { background-color: #bdbdbd; cursor: not-allowed; } h2 { margin-top: 0; color: #1976d2; } .visualizer { width: 100%; height: 100px; margin: 10px 0; background: #fafafa; border-radius: 8px; } /* Add styles for disclaimer */ .disclaimer { background-color: #fff3e0; padding: 15px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; line-height: 1.5; max-width: 1400px; margin: 0 auto 20px auto; } /* Update nav bar styles */ .nav-bar { background-color: #f5f5f5; padding: 10px 20px; margin-bottom: 20px; } .nav-container { max-width: 1400px; margin: 0 auto; display: flex; gap: 10px; } .nav-button { background-color: #1976d2; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 14px; transition: background-color 0.2s; } .nav-button:hover { background-color: #1565c0; } /* Add styles for toast notifications */ .toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 16px 24px; border-radius: 4px; font-size: 14px; z-index: 1000; display: none; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .toast.error { background-color: #f44336; color: white; } .toast.warning { background-color: #ffd700; color: black; } </style> </head> <body> <nav class="nav-bar"> <div class="nav-container"> <a href="./webrtc/docs" class="nav-button">WebRTC Docs</a> <a href="./websocket/docs" class="nav-button">WebSocket Docs</a> <a href="./telephone/docs" class="nav-button">Telephone Docs</a> <a href="./ui" class="nav-button">UI</a> </div> </nav> <div class="disclaimer"> This page compares the WebRTC Round-Trip-Time calculated from <code>getStats()</code> to the time taken to process a ping/pong response pattern over websockets. It may not be a gold standard benchmark. Both WebRTC and Websockets have their merits/advantages which is why FastRTC supports both. Artifacts in the WebSocket playback audio are due to gaps in my frontend processing code and not FastRTC web server. </div> <div class="container"> <div class="panel"> <h2>WebRTC Connection</h2> <div id="webrtc-chat" class="chat-container"></div> <div id="webrtc-metrics" class="metrics"> <div class="metric">RTT (Round Trip Time): <span id="webrtc-rtt">-</span></div> </div> <button id="webrtc-button">Connect WebRTC</button> </div> <div class="panel"> <h2>WebSocket Connection</h2> <div id="ws-chat" class="chat-container"></div> <div id="ws-metrics" class="metrics"> <div class="metric">RTT (Round Trip Time): <span id="ws-rtt">0</span></div> </div> <button id="ws-button">Connect WebSocket</button> </div> </div> <audio id="webrtc-audio" style="display: none;"></audio> <audio id="ws-audio" style="display: none;"></audio> <div id="error-toast" class="toast"></div> <script> // Shared utilities function generateId() { return Math.random().toString(36).substring(7); } function sendInput(id) { return function handleMessage(event) { const eventJson = JSON.parse(event.data); if (eventJson.type === "send_input") { fetch('/input_hook', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ webrtc_id: id, chatbot: chatHistoryWebRTC }) }); } } } let chatHistoryWebRTC = []; let chatHistoryWebSocket = []; function addMessage(containerId, role, content) { const container = document.getElementById(containerId); const messageDiv = document.createElement('div'); messageDiv.classList.add('message', role); messageDiv.textContent = content; container.appendChild(messageDiv); container.scrollTop = container.scrollHeight; if (containerId === 'webrtc-chat') { chatHistoryWebRTC.push({ role, content }); } else { chatHistoryWebSocket.push({ role, content }); } } // WebRTC Implementation let webrtcPeerConnection; // Add this function to collect RTT stats async function updateWebRTCStats() { if (!webrtcPeerConnection) return; const stats = await webrtcPeerConnection.getStats(); stats.forEach(report => { if (report.type === 'candidate-pair' && report.state === 'succeeded') { const rtt = report.currentRoundTripTime * 1000; // Convert to ms document.getElementById('webrtc-rtt').textContent = `${rtt.toFixed(2)}ms`; } }); } async function setupWebRTC() { const button = document.getElementById('webrtc-button'); button.textContent = "Stop"; const config = __RTC_CONFIGURATION__; webrtcPeerConnection = new RTCPeerConnection(config); const webrtcId = generateId(); const timeoutId = setTimeout(() => { const toast = document.getElementById('error-toast'); toast.textContent = "Connection is taking longer than usual. Are you on a VPN?"; toast.className = 'toast warning'; toast.style.display = 'block'; // Hide warning after 5 seconds setTimeout(() => { toast.style.display = 'none'; }, 5000); }, 5000); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); stream.getTracks().forEach(track => { webrtcPeerConnection.addTrack(track, stream); }); webrtcPeerConnection.addEventListener('track', (evt) => { const audio = document.getElementById('webrtc-audio'); if (audio.srcObject !== evt.streams[0]) { audio.srcObject = evt.streams[0]; audio.play(); } }); const dataChannel = webrtcPeerConnection.createDataChannel('text'); dataChannel.onmessage = sendInput(webrtcId); const offer = await webrtcPeerConnection.createOffer(); await webrtcPeerConnection.setLocalDescription(offer); await new Promise((resolve) => { if (webrtcPeerConnection.iceGatheringState === "complete") { resolve(); } else { const checkState = () => { if (webrtcPeerConnection.iceGatheringState === "complete") { webrtcPeerConnection.removeEventListener("icegatheringstatechange", checkState); resolve(); } }; webrtcPeerConnection.addEventListener("icegatheringstatechange", checkState); } }); const response = await fetch('/webrtc/offer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sdp: webrtcPeerConnection.localDescription.sdp, type: webrtcPeerConnection.localDescription.type, webrtc_id: webrtcId }) }); const serverResponse = await response.json(); await webrtcPeerConnection.setRemoteDescription(serverResponse); // Setup event source for messages const eventSource = new EventSource('/outputs?webrtc_id=' + webrtcId); eventSource.addEventListener("output", (event) => { const eventJson = JSON.parse(event.data); addMessage('webrtc-chat', eventJson.role, eventJson.content); }); // Add periodic stats collection const statsInterval = setInterval(updateWebRTCStats, 1000); // Store the interval ID on the connection webrtcPeerConnection.statsInterval = statsInterval; webrtcPeerConnection.addEventListener('connectionstatechange', () => { if (webrtcPeerConnection.connectionState === 'connected') { clearTimeout(timeoutId); const toast = document.getElementById('error-toast'); toast.style.display = 'none'; } }); } catch (err) { clearTimeout(timeoutId); console.error('WebRTC setup error:', err); } } function webrtc_stop() { if (webrtcPeerConnection) { // Clear the stats interval if (webrtcPeerConnection.statsInterval) { clearInterval(webrtcPeerConnection.statsInterval); } // Close all tracks webrtcPeerConnection.getSenders().forEach(sender => { if (sender.track) { sender.track.stop(); } }); webrtcPeerConnection.close(); webrtcPeerConnection = null; // Reset metrics display document.getElementById('webrtc-rtt').textContent = '-'; } } // WebSocket Implementation let webSocket; let wsMetrics = { pingStartTime: 0, rttValues: [] }; // Load mu-law library // Add load promise to track when the script is ready function resample(audioData, fromSampleRate, toSampleRate) { const ratio = fromSampleRate / toSampleRate; const newLength = Math.round(audioData.length / ratio); const result = new Float32Array(newLength); for (let i = 0; i < newLength; i++) { const position = i * ratio; const index = Math.floor(position); const fraction = position - index; if (index + 1 < audioData.length) { result[i] = audioData[index] * (1 - fraction) + audioData[index + 1] * fraction; } else { result[i] = audioData[index]; } } return result; } function convertToMulaw(audioData, sampleRate) { // Resample to 8000 Hz if needed if (sampleRate !== 8000) { audioData = resample(audioData, sampleRate, 8000); } // Convert float32 [-1,1] to int16 [-32768,32767] const int16Data = new Int16Array(audioData.length); for (let i = 0; i < audioData.length; i++) { int16Data[i] = Math.floor(audioData[i] * 32768); } // Convert to mu-law using the library return alawmulaw.mulaw.encode(int16Data); } async function setupWebSocket() { const button = document.getElementById('ws-button'); button.textContent = "Stop"; try { const stream = await navigator.mediaDevices.getUserMedia({ audio: { "echoCancellation": true, "noiseSuppression": { "exact": true }, "autoGainControl": { "exact": true }, "sampleRate": { "ideal": 24000 }, "sampleSize": { "ideal": 16 }, "channelCount": { "exact": 1 }, } }); const wsId = generateId(); wsMetrics.startTime = performance.now(); // Create audio context and analyser for visualization const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); const source = audioContext.createMediaStreamSource(stream); source.connect(analyser); // Connect to websocket endpoint webSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/websocket/offer`); webSocket.onopen = () => { // Send initial start message webSocket.send(JSON.stringify({ event: "start", websocket_id: wsId })); // Setup audio processing const processor = audioContext.createScriptProcessor(2048, 1, 1); source.connect(processor); processor.connect(audioContext.destination); processor.onaudioprocess = (e) => { const inputData = e.inputBuffer.getChannelData(0); const mulawData = convertToMulaw(inputData, audioContext.sampleRate); const base64Audio = btoa(String.fromCharCode.apply(null, mulawData)); if (webSocket.readyState === WebSocket.OPEN) { webSocket.send(JSON.stringify({ event: "media", media: { payload: base64Audio } })); } }; // Add ping interval webSocket.pingInterval = setInterval(() => { wsMetrics.pingStartTime = performance.now(); webSocket.send(JSON.stringify({ event: "ping" })); }, 500); }; // Setup audio output context const outputContext = new AudioContext({ sampleRate: 24000 }); const sampleRate = 24000; // Updated to match server sample rate let audioQueue = []; let isPlaying = false; webSocket.onmessage = (event) => { const data = JSON.parse(event.data); if (data?.type === "send_input") { console.log("sending input") fetch('/input_hook', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ webrtc_id: wsId, chatbot: chatHistoryWebSocket }) }); } if (data.event === "media") { // Process received audio const audioData = atob(data.media.payload); const mulawData = new Uint8Array(audioData.length); for (let i = 0; i < audioData.length; i++) { mulawData[i] = audioData.charCodeAt(i); } // Convert mu-law to linear PCM const linearData = alawmulaw.mulaw.decode(mulawData); // Create an AudioBuffer const audioBuffer = outputContext.createBuffer(1, linearData.length, sampleRate); const channelData = audioBuffer.getChannelData(0); // Fill the buffer with the decoded data for (let i = 0; i < linearData.length; i++) { channelData[i] = linearData[i] / 32768.0; } // Queue the audio buffer audioQueue.push(audioBuffer); // Start playing if not already playing if (!isPlaying) { playNextBuffer(); } } // Add pong handler if (data.event === "pong") { const rtt = performance.now() - wsMetrics.pingStartTime; wsMetrics.rttValues.push(rtt); // Keep only last 20 values for running mean if (wsMetrics.rttValues.length > 20) { wsMetrics.rttValues.shift(); } const avgRtt = wsMetrics.rttValues.reduce((a, b) => a + b, 0) / wsMetrics.rttValues.length; document.getElementById('ws-rtt').textContent = `${avgRtt.toFixed(2)}ms`; return; } }; function playNextBuffer() { if (audioQueue.length === 0) { isPlaying = false; return; } isPlaying = true; const bufferSource = outputContext.createBufferSource(); bufferSource.buffer = audioQueue.shift(); bufferSource.connect(outputContext.destination); bufferSource.onended = playNextBuffer; bufferSource.start(); } const eventSource = new EventSource('/outputs?webrtc_id=' + wsId); eventSource.addEventListener("output", (event) => { console.log("ws output", event); const eventJson = JSON.parse(event.data); addMessage('ws-chat', eventJson.role, eventJson.content); }); } catch (err) { console.error('WebSocket setup error:', err); button.disabled = false; } } function ws_stop() { if (webSocket) { webSocket.send(JSON.stringify({ event: "stop" })); // Clear ping interval if (webSocket.pingInterval) { clearInterval(webSocket.pingInterval); } // Reset RTT display document.getElementById('ws-rtt').textContent = '-'; wsMetrics.rttValues = []; // Clear the stats interval if (webSocket.statsInterval) { clearInterval(webSocket.statsInterval); } webSocket.close(); } } // Event Listeners document.getElementById('webrtc-button').addEventListener('click', () => { const button = document.getElementById('webrtc-button'); if (button.textContent === 'Connect WebRTC') { setupWebRTC(); } else { webrtc_stop(); button.textContent = 'Connect WebRTC'; } }); const ws_start_button = document.getElementById('ws-button') ws_start_button.addEventListener('click', () => { if (ws_start_button.textContent === 'Connect WebSocket') { setupWebSocket(); ws_start_button.textContent = 'Stop'; } else { ws_stop(); ws_start_button.textContent = 'Connect WebSocket'; } }); document.addEventListener("beforeunload", () => { ws_stop(); webrtc_stop(); }); </script> </body> </html>