vicinix
v1.0.3
Published
React hooks for measuring latency using WebSocket and WebRTC
Maintainers
Readme
Vicinix - WebRTC Proximity Detection Hooks
A lightweight React hook-based library for detecting device proximity using WebRTC data channels and WebSocket signaling.
Author
Description
Vicinix enables proximity detection between devices by measuring Round-Trip Time (RTT) over WebRTC data channels. It provides two React hooks:
useAdmin: Manages connections with client devices and monitors their proximity based on RTT.useClient: Allows client devices to connect to an admin and perform proximity tests.
Proximity is determined by sending multiple pings and checking if a sufficient percentage of RTTs are below a configurable threshold, indicating the devices are "nearby."
Installation
Install the package via npm:
npm install vicinixUsage
Both hooks require a WebSocket URL for signaling and an ICE servers configuration for WebRTC peer connections.
Admin Hook
The useAdmin hook is used to monitor and manage client connections.
import { useAdmin } from 'vicinix';
function AdminDashboard() {
const { status, nearbyDevices, iceState } = useAdmin({
wsUrl: 'wss://your-signaling-server.com',
iceServers: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
// Additional STUN/TURN servers
]
}
});
return (
<div className="p-4">
<h1 className="text-2xl font-bold">Admin Dashboard</h1>
<p><strong>Status:</strong> {status}</p>
<h2 className="mt-4 text-xl">Nearby Devices</h2>
<ul className="list-disc pl-5">
{nearbyDevices.map(device => (
<li key={device.id}>
{device.id} - RTT: {device.rtt}ms {device.rtt < 20 ? '✅' : '❌'}
</li>
))}
</ul>
<p className="mt-4"><strong>ICE State:</strong> {iceState}</p>
</div>
);
}Client Hook
The useClient hook allows a client to register with an admin and perform proximity tests.
import { useState } from 'react';
import { useClient } from 'vicinix';
function ClientInterface() {
const [clientId, setClientId] = useState('');
const { status, result, rtts, iceState, register, sendPing } = useClient({
wsUrl: 'wss://your-signaling-server.com',
iceServers: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
// Additional STUN/TURN servers
]
},
rttThreshold: 20, // Max RTT for proximity
acceptanceThreshold: 60 // Min % of pings under threshold
});
return (
<div className="p-4">
<h1 className="text-2xl font-bold">Client Interface</h1>
<div className="mt-4">
<input
type="text"
value={clientId}
onChange={(e) => setClientId(e.target.value)}
placeholder="Enter Client ID"
className="border p-2 rounded"
/>
<button
onClick={() => register(clientId)}
className="ml-2 bg-blue-500 text-white p-2 rounded"
disabled={!clientId}
>
Register
</button>
<button
onClick={sendPing}
className="ml-2 bg-green-500 text-white p-2 rounded"
disabled={!status.includes('Registered')}
>
Test Proximity
</button>
</div>
<p className="mt-4"><strong>Status:</strong> {status}</p>
{result && <p><strong>Result:</strong> {result}</p>}
{rtts.length > 0 && (
<div className="mt-4">
<h2 className="text-xl">RTT Measurements</h2>
<ul className="list-disc pl-5">
{rtts.map((rtt, index) => (
<li key={index}>
Sample #{index + 1}: {rtt ? `${rtt}ms` : 'Timeout'} {rtt && rtt < 20 ? '✅' : '❌'}
</li>
))}
</ul>
</div>
)}
<p className="mt-4"><strong>ICE State:</strong> {iceState}</p>
</div>
);
}API
useAdmin
Parameters:
wsUrl: WebSocket URL for the signaling server (required).iceServers: WebRTC ICE servers configuration (required).
Returns:
status: Connection status (e.g., "Ready to connect", "Data channel open").nearbyDevices: Array of{ id, rtt }objects for connected clients.iceState: ICE connection state updates.
useClient
Parameters:
wsUrl: WebSocket URL for the signaling server (required).iceServers: WebRTC ICE servers configuration (required).rttThreshold: Max RTT (ms) for a ping to be considered nearby (default: 20).acceptanceThreshold: Min % of pings belowrttThresholdfor proximity (default: 60).
Returns:
status: Connection status (e.g., "Registered as client", "Answer sent").result: Proximity test result ('✅ Accepted' or '❌ Rejected').rtts: Array of RTT values (ms) or null for timeouts.iceState: ICE connection state updates.register: Function to register with a client ID.sendPing: Function to initiate a proximity test.
Signaling Server
A WebSocket signaling server is required to coordinate WebRTC connections. Below is a simplified implementation:
const http = require('http');
const WebSocket = require('ws');
const PORT = process.env.PORT || 8080;
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Server is live' }));
}
});
const wss = new WebSocket.Server({ server });
const clients = { admin: null, clients: new Map(), clientWebSockets: new Map() };
const processedMessages = new Set();
wss.on('connection', (ws) => {
ws.on('message', (message) => {
try {
const msg = JSON.parse(message);
const messageId = msg.messageId || `${msg.type}-${msg.id || 'unknown'}-${Date.now()}`;
if (processedMessages.has(messageId)) return;
processedMessages.add(messageId);
if (msg.type === 'register') {
if (msg.role === 'admin') {
clients.admin = ws;
ws.send(JSON.stringify({ type: 'registered', role: 'admin', messageId }));
} else if (msg.role === 'client' && msg.id) {
clients.clients.set(msg.id, ws);
clients.clientWebSockets.set(ws, msg.id);
if (clients.admin?.readyState === WebSocket.OPEN) {
clients.admin.send(JSON.stringify({ type: 'new-client', id: msg.id, messageId }));
}
ws.send(JSON.stringify({ type: 'registered', role: 'client', id: msg.id, messageId }));
} else {
ws.send(JSON.stringify({ type: 'error', message: 'Client ID required', messageId }));
}
return;
}
const senderId = ws === clients.admin ? 'admin' : clients.clientWebSockets.get(ws);
if (msg.target === 'admin' && clients.admin?.readyState === WebSocket.OPEN) {
clients.admin.send(JSON.stringify({ ...msg, senderId, messageId }));
} else if (msg.target === 'client') {
const targetWs = clients.clients.get(msg.targetId);
if (targetWs?.readyState === WebSocket.OPEN) {
targetWs.send(JSON.stringify({ ...msg, senderId, messageId }));
}
}
} catch (error) {
console.error('Message error:', error);
}
});
ws.on('close', () => {
if (ws === clients.admin) {
clients.admin = null;
} else {
const clientId = clients.clientWebSockets.get(ws);
if (clientId) {
clients.clients.delete(clientId);
clients.clientWebSockets.delete(ws);
clients.admin?.send(JSON.stringify({ type: 'client-disconnected', id: clientId, messageId: `disconnect-${clientId}-${Date.now()}` }));
}
}
});
});
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));Running the Signaling Server
Install dependencies:
npm install wsSave the code as
server.js.Run the server:
node server.jsUpdate
wsUrlin your hooks to match the server (e.g.,ws://localhost:8080).
How It Works
- Admin Initialization: The admin connects to the signaling server and waits for clients.
- Client Registration: Clients register with a unique ID, notifying the admin via the signaling server.
- WebRTC Setup: The admin initiates a peer connection, exchanging ICE candidates and session descriptions through the signaling server.
- Proximity Test: The client sends 10 pings at 100ms intervals over the data channel. After 2 seconds, it evaluates if enough pings meet the
rttThreshold. - Results: The admin sees RTTs for each client, while the client receives a proximity result.
Configuration
- ICE Servers: Use public STUN servers or configure TURN servers for better connectivity.
- Proximity Parameters:
rttThreshold: Max RTT (ms) for a ping to be "nearby" (default: 20).acceptanceThreshold: Min % of successful pings (default: 60).
Dependencies
react(peer dependency)ws(for the signaling server)
License
MIT License
