webrtc-vs-websocket / index.html
freddyaboulton's picture
Upload folder using huggingface_hub
1e421f2 verified
<!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>