uniwrtc 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -30
- package/client-browser.js +9 -0
- package/demo.html +1 -1
- package/index.html +12 -0
- package/package.json +9 -2
- package/public/favicon.ico +1 -0
- package/src/index.js +50 -8
- package/src/main.js +379 -0
- package/src/room.js +5 -0
- package/src/style.css +210 -0
- package/vite.config.js +13 -0
- package/wrangler.toml +10 -1
package/README.md
CHANGED
|
@@ -53,14 +53,21 @@ PORT=8080
|
|
|
53
53
|
|
|
54
54
|
### Try the Demo
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
The interactive demo is available live at **https://signal.peer.ooo/** (Cloudflare Workers deployment) or run locally:
|
|
57
|
+
|
|
58
|
+
**Using the deployed demo (recommended):**
|
|
59
|
+
1. Open https://signal.peer.ooo/ in two browser tabs
|
|
60
|
+
2. Default room is `demo-room`—both tabs will auto-connect
|
|
61
|
+
3. Click "Connect" to join
|
|
62
|
+
4. Watch the activity log to see peers connecting
|
|
63
|
+
5. Open the P2P chat and send messages between tabs
|
|
64
|
+
|
|
65
|
+
**Or run locally:**
|
|
66
|
+
1. Start the server: `npm start` (signaling at `ws://localhost:8080`)
|
|
67
|
+
2. Start the Vite dev server: `npm run dev` (demo at `http://localhost:5173/`)
|
|
68
|
+
3. Open the demo in two browser tabs
|
|
69
|
+
4. Enter the same session ID in both, then Connect
|
|
70
|
+
5. Chat P2P once data channels open
|
|
64
71
|
|
|
65
72
|
## Usage
|
|
66
73
|
|
|
@@ -166,10 +173,11 @@ The signaling server accepts WebSocket connections and supports the following me
|
|
|
166
173
|
|
|
167
174
|
Use directly from npm:
|
|
168
175
|
```javascript
|
|
169
|
-
// ESM
|
|
170
|
-
import
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
// ESM (browser)
|
|
177
|
+
import UniWRTCClient from 'uniwrtc/client-browser.js';
|
|
178
|
+
|
|
179
|
+
// CommonJS (Node.js)
|
|
180
|
+
const UniWRTCClient = require('uniwrtc/client.js');
|
|
173
181
|
```
|
|
174
182
|
|
|
175
183
|
The `client.js` library provides a convenient wrapper for the signaling protocol:
|
|
@@ -211,8 +219,8 @@ client.on('ice-candidate', (data) => {
|
|
|
211
219
|
// Connect to the server
|
|
212
220
|
await client.connect();
|
|
213
221
|
|
|
214
|
-
// Join a
|
|
215
|
-
client.
|
|
222
|
+
// Join a session
|
|
223
|
+
client.joinSession('my-session');
|
|
216
224
|
|
|
217
225
|
// Send WebRTC signaling messages
|
|
218
226
|
client.sendOffer(offerObject, targetPeerId);
|
|
@@ -298,6 +306,11 @@ client.on('ice-candidate', async (data) => {
|
|
|
298
306
|
// Connect and join session
|
|
299
307
|
await client.connect();
|
|
300
308
|
client.joinSession('my-video-session');
|
|
309
|
+
|
|
310
|
+
// Or use Cloudflare Durable Objects deployment
|
|
311
|
+
const cfClient = new UniWRTCClient('wss://signal.peer.ooo?room=my-session');
|
|
312
|
+
await cfClient.connect();
|
|
313
|
+
cfClient.joinSession('my-session');
|
|
301
314
|
```
|
|
302
315
|
|
|
303
316
|
## API Reference
|
|
@@ -319,12 +332,12 @@ new UniWRTCClient(serverUrl, options)
|
|
|
319
332
|
|
|
320
333
|
- `connect()`: Connect to the signaling server (returns Promise)
|
|
321
334
|
- `disconnect()`: Disconnect from the server
|
|
322
|
-
- `joinSession(sessionId)`: Join a specific session
|
|
335
|
+
- `joinSession(sessionId)`: Join a specific session (peers isolated by session)
|
|
323
336
|
- `leaveSession()`: Leave the current session
|
|
324
|
-
- `sendOffer(offer, targetId)`: Send a WebRTC offer
|
|
325
|
-
- `sendAnswer(answer, targetId)`: Send a WebRTC answer
|
|
326
|
-
- `sendIceCandidate(candidate, targetId)`: Send an ICE candidate
|
|
327
|
-
- `listRooms()`: Request list of available
|
|
337
|
+
- `sendOffer(offer, targetId)`: Send a WebRTC offer to a specific peer
|
|
338
|
+
- `sendAnswer(answer, targetId)`: Send a WebRTC answer to a specific peer
|
|
339
|
+
- `sendIceCandidate(candidate, targetId)`: Send an ICE candidate to a specific peer
|
|
340
|
+
- `listRooms()`: Request list of available sessions (legacy)
|
|
328
341
|
- `on(event, handler)`: Register event handler
|
|
329
342
|
- `off(event, handler)`: Unregister event handler
|
|
330
343
|
|
|
@@ -359,21 +372,22 @@ Response:
|
|
|
359
372
|
|
|
360
373
|
## Architecture
|
|
361
374
|
|
|
362
|
-
### Session
|
|
375
|
+
### Session-based Peer Isolation
|
|
363
376
|
|
|
364
|
-
- Each session is identified by a unique
|
|
365
|
-
-
|
|
366
|
-
-
|
|
367
|
-
-
|
|
377
|
+
- **Sessions**: Each session is identified by a unique string ID (also called "room" in the UI)
|
|
378
|
+
- **Peer routing**: Each peer gets a unique client ID; signaling messages are routed only to intended targets
|
|
379
|
+
- **Session isolation**: Peers in different sessions cannot see or communicate with each other
|
|
380
|
+
- **Cloudflare Durable Objects**: Uses DO state to isolate sessions; routing by `?room=` query param per session
|
|
381
|
+
- Clients join with `joinSession(sessionId)` and receive notifications when other peers join the same session
|
|
368
382
|
|
|
369
383
|
### Message Flow
|
|
370
384
|
|
|
371
|
-
1. Client connects via WebSocket
|
|
372
|
-
2. Server assigns a unique client ID
|
|
373
|
-
3. Client
|
|
374
|
-
4. Server
|
|
375
|
-
5. Peers exchange WebRTC
|
|
376
|
-
6. Server routes messages
|
|
385
|
+
1. Client connects via WebSocket (or WS-over-HTTP for Cloudflare)
|
|
386
|
+
2. Server/Durable Object assigns a unique client ID
|
|
387
|
+
3. Client sends join message with session ID
|
|
388
|
+
4. Server broadcasts `peer-joined` to other peers in the same session only
|
|
389
|
+
5. Peers exchange WebRTC offers/answers/ICE candidates via the server
|
|
390
|
+
6. Server routes signaling messages to specific peers by target ID (unicast, not broadcast)
|
|
377
391
|
|
|
378
392
|
## Security Considerations
|
|
379
393
|
|
package/client-browser.js
CHANGED
|
@@ -125,6 +125,7 @@ class UniWRTCClient {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
sendOffer(offer, targetId) {
|
|
128
|
+
console.log(`[Client] Sending offer to ${targetId}`);
|
|
128
129
|
this.send({
|
|
129
130
|
type: 'offer',
|
|
130
131
|
offer: offer,
|
|
@@ -134,6 +135,7 @@ class UniWRTCClient {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
sendAnswer(answer, targetId) {
|
|
138
|
+
console.log(`[Client] Sending answer to ${targetId}`);
|
|
137
139
|
this.send({
|
|
138
140
|
type: 'answer',
|
|
139
141
|
answer: answer,
|
|
@@ -143,6 +145,7 @@ class UniWRTCClient {
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
sendIceCandidate(candidate, targetId) {
|
|
148
|
+
console.log(`[Client] Sending ICE candidate to ${targetId}`);
|
|
146
149
|
this.send({
|
|
147
150
|
type: 'ice-candidate',
|
|
148
151
|
candidate: candidate,
|
|
@@ -210,18 +213,21 @@ class UniWRTCClient {
|
|
|
210
213
|
});
|
|
211
214
|
break;
|
|
212
215
|
case 'offer':
|
|
216
|
+
console.log(`[Client] Received offer from ${message.peerId}`);
|
|
213
217
|
this.emit('offer', {
|
|
214
218
|
peerId: message.peerId,
|
|
215
219
|
offer: message.offer
|
|
216
220
|
});
|
|
217
221
|
break;
|
|
218
222
|
case 'answer':
|
|
223
|
+
console.log(`[Client] Received answer from ${message.peerId}`);
|
|
219
224
|
this.emit('answer', {
|
|
220
225
|
peerId: message.peerId,
|
|
221
226
|
answer: message.answer
|
|
222
227
|
});
|
|
223
228
|
break;
|
|
224
229
|
case 'ice-candidate':
|
|
230
|
+
console.log(`[Client] Received ICE candidate from ${message.peerId}`);
|
|
225
231
|
this.emit('ice-candidate', {
|
|
226
232
|
peerId: message.peerId,
|
|
227
233
|
candidate: message.candidate
|
|
@@ -254,3 +260,6 @@ class UniWRTCClient {
|
|
|
254
260
|
if (typeof window !== 'undefined') {
|
|
255
261
|
window.UniWRTCClient = UniWRTCClient;
|
|
256
262
|
}
|
|
263
|
+
|
|
264
|
+
// ESM default export for bundlers like Vite
|
|
265
|
+
export default UniWRTCClient;
|
package/demo.html
CHANGED
|
@@ -314,7 +314,7 @@
|
|
|
314
314
|
<p style="color: #999; text-align: center; padding: 20px;">Messages will appear here</p>
|
|
315
315
|
</div>
|
|
316
316
|
<div class="connection-controls" style="margin-bottom: 0;">
|
|
317
|
-
<input type="text" id="chatMessage" placeholder="Type a message..." style="grid-column: 1 / -1;">
|
|
317
|
+
<input type="text" id="chatMessage" placeholder="Type a message..." style="grid-column: 1 / -1;" onkeypress="if(event.key === 'Enter') sendChatMessage()">
|
|
318
318
|
<button class="btn-primary" onclick="sendChatMessage()" style="grid-column: 1 / -1;">Send Message</button>
|
|
319
319
|
</div>
|
|
320
320
|
</div> <div class="card">
|
package/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>UniWRTC Demo - WebRTC Signaling</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniwrtc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "A universal WebRTC signaling service",
|
|
5
5
|
"main": "server.js",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"start": "node server.js",
|
|
8
|
-
"dev": "
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"server": "node server.js",
|
|
9
13
|
"test": "node test.js"
|
|
10
14
|
},
|
|
11
15
|
"keywords": [
|
|
@@ -20,6 +24,9 @@
|
|
|
20
24
|
"dependencies": {
|
|
21
25
|
"ws": "^8.17.1"
|
|
22
26
|
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"vite": "^6.0.6"
|
|
29
|
+
},
|
|
23
30
|
"engines": {
|
|
24
31
|
"node": ">=14.0.0"
|
|
25
32
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
UNI WRTC ICON
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UniWRTC Cloudflare Worker
|
|
3
3
|
* WebRTC Signaling Service using Durable Objects
|
|
4
|
+
* Serves both static assets and WebSocket signaling
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { Room } from './room.js';
|
|
@@ -10,6 +11,7 @@ export { Room };
|
|
|
10
11
|
export default {
|
|
11
12
|
async fetch(request, env) {
|
|
12
13
|
const url = new URL(request.url);
|
|
14
|
+
console.log(`[Worker] ${request.method} ${url.pathname}${url.search}`);
|
|
13
15
|
|
|
14
16
|
// Health check
|
|
15
17
|
if (url.pathname === '/health') {
|
|
@@ -18,16 +20,56 @@ export default {
|
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
// WebSocket
|
|
22
|
-
if (url.pathname === '/
|
|
23
|
-
|
|
23
|
+
// Handle WebSocket upgrade on /ws endpoint
|
|
24
|
+
if (url.pathname === '/ws') {
|
|
25
|
+
if (request.headers.get('Upgrade') === 'websocket') {
|
|
26
|
+
console.log(`[Worker] WebSocket upgrade detected for room: ${url.searchParams.get('room')}`);
|
|
27
|
+
const roomId = url.searchParams.get('room') || 'default';
|
|
28
|
+
const id = env.ROOMS.idFromName(roomId);
|
|
29
|
+
const roomStub = env.ROOMS.get(id);
|
|
30
|
+
console.log(`[Worker] Routing to Durable Object: ${roomId}`);
|
|
31
|
+
return roomStub.fetch(request);
|
|
32
|
+
}
|
|
33
|
+
return new Response('WebSocket upgrade required', { status: 400 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Serve root as index.html
|
|
37
|
+
if (url.pathname === '/') {
|
|
38
|
+
try {
|
|
39
|
+
const asset = await env.ASSETS.get('index.html');
|
|
40
|
+
if (asset) {
|
|
41
|
+
return new Response(asset, {
|
|
42
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// ASSETS binding may not exist in local dev
|
|
47
|
+
}
|
|
48
|
+
}
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
// Serve static assets (CSS, JS, images, etc.)
|
|
51
|
+
if (request.method === 'GET') {
|
|
52
|
+
try {
|
|
53
|
+
const pathname = url.pathname.replace(/^\//, '');
|
|
54
|
+
const asset = await env.ASSETS.get(pathname);
|
|
55
|
+
if (asset) {
|
|
56
|
+
let contentType = 'application/octet-stream';
|
|
57
|
+
if (pathname.endsWith('.html')) contentType = 'text/html; charset=utf-8';
|
|
58
|
+
else if (pathname.endsWith('.js')) contentType = 'application/javascript';
|
|
59
|
+
else if (pathname.endsWith('.css')) contentType = 'text/css';
|
|
60
|
+
else if (pathname.endsWith('.json')) contentType = 'application/json';
|
|
61
|
+
else if (pathname.endsWith('.svg')) contentType = 'image/svg+xml';
|
|
62
|
+
else if (pathname.endsWith('.png')) contentType = 'image/png';
|
|
63
|
+
else if (pathname.endsWith('.jpg')) contentType = 'image/jpeg';
|
|
64
|
+
else if (pathname.endsWith('.ico')) contentType = 'image/x-icon';
|
|
28
65
|
|
|
29
|
-
|
|
30
|
-
|
|
66
|
+
return new Response(asset, {
|
|
67
|
+
headers: { 'Content-Type': contentType }
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// ASSETS binding may not exist in local dev
|
|
72
|
+
}
|
|
31
73
|
}
|
|
32
74
|
|
|
33
75
|
return new Response('Not Found', { status: 404 });
|
package/src/main.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import './style.css';
|
|
2
|
+
import UniWRTCClient from '../client-browser.js';
|
|
3
|
+
|
|
4
|
+
// Make UniWRTCClient available globally for backwards compatibility
|
|
5
|
+
window.UniWRTCClient = UniWRTCClient;
|
|
6
|
+
|
|
7
|
+
let client = null;
|
|
8
|
+
const peerConnections = new Map();
|
|
9
|
+
const dataChannels = new Map();
|
|
10
|
+
|
|
11
|
+
// Initialize app
|
|
12
|
+
document.getElementById('app').innerHTML = `
|
|
13
|
+
<div class="container">
|
|
14
|
+
<div class="header">
|
|
15
|
+
<h1>🌐 UniWRTC Demo</h1>
|
|
16
|
+
<p>WebRTC Signaling made simple</p>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="card">
|
|
20
|
+
<h2>Connection</h2>
|
|
21
|
+
<div class="connection-controls">
|
|
22
|
+
<div>
|
|
23
|
+
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Server URL</label>
|
|
24
|
+
<input type="text" id="serverUrl" placeholder="wss://signal.peer.ooo or ws://localhost:8080" value="wss://signal.peer.ooo">
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Room / Session ID</label>
|
|
28
|
+
<input type="text" id="roomId" placeholder="my-room">
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div style="display: flex; gap: 10px; align-items: center;">
|
|
32
|
+
<button onclick="window.connect()" class="btn-primary" id="connectBtn">Connect</button>
|
|
33
|
+
<button onclick="window.disconnect()" class="btn-danger" id="disconnectBtn" disabled>Disconnect</button>
|
|
34
|
+
<span id="statusBadge" class="status-badge status-disconnected">Disconnected</span>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="card">
|
|
39
|
+
<h2>Client Info</h2>
|
|
40
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
|
41
|
+
<div>
|
|
42
|
+
<strong style="color: #64748b;">Client ID:</strong>
|
|
43
|
+
<div id="clientId" style="font-family: monospace; color: #333; margin-top: 5px;">Not connected</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div>
|
|
46
|
+
<strong style="color: #64748b;">Session ID:</strong>
|
|
47
|
+
<div id="sessionId" style="font-family: monospace; color: #333; margin-top: 5px;">Not joined</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="card">
|
|
53
|
+
<h2>Connected Peers</h2>
|
|
54
|
+
<div id="peerList" class="peer-list">
|
|
55
|
+
<p style="color: #94a3b8;">No peers connected</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="card">
|
|
60
|
+
<h2>Peer-to-Peer Chat</h2>
|
|
61
|
+
<div id="chatContainer">
|
|
62
|
+
<p>Connect to a room and wait for peers to start chatting</p>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="chat-controls">
|
|
65
|
+
<input type="text" id="chatMessage" placeholder="Type a message..." onkeypress="if(event.key === 'Enter') window.sendChatMessage()">
|
|
66
|
+
<button onclick="window.sendChatMessage()" class="btn-primary">Send</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="card">
|
|
71
|
+
<h2>Activity Log</h2>
|
|
72
|
+
<div id="logContainer" class="log-container">
|
|
73
|
+
<div class="log-entry success">UniWRTC Demo ready</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
// Prefill room input from URL (?room= or ?session=); otherwise set a visible default the user can override
|
|
80
|
+
const roomInput = document.getElementById('roomId');
|
|
81
|
+
const params = new URLSearchParams(window.location.search);
|
|
82
|
+
const urlRoom = params.get('room') || params.get('session');
|
|
83
|
+
if (urlRoom) {
|
|
84
|
+
roomInput.value = urlRoom;
|
|
85
|
+
log(`Prefilled room ID from URL: ${urlRoom}`, 'info');
|
|
86
|
+
} else {
|
|
87
|
+
const defaultRoom = 'demo-room';
|
|
88
|
+
roomInput.value = defaultRoom;
|
|
89
|
+
log(`Using default room ID: ${defaultRoom}`, 'info');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function log(message, type = 'info') {
|
|
93
|
+
const logContainer = document.getElementById('logContainer');
|
|
94
|
+
const entry = document.createElement('div');
|
|
95
|
+
entry.className = `log-entry ${type}`;
|
|
96
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
97
|
+
entry.textContent = `[${timestamp}] ${message}`;
|
|
98
|
+
logContainer.appendChild(entry);
|
|
99
|
+
logContainer.scrollTop = logContainer.scrollHeight;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function updateStatus(connected) {
|
|
103
|
+
const badge = document.getElementById('statusBadge');
|
|
104
|
+
const connectBtn = document.getElementById('connectBtn');
|
|
105
|
+
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
106
|
+
|
|
107
|
+
if (connected) {
|
|
108
|
+
badge.textContent = 'Connected';
|
|
109
|
+
badge.className = 'status-badge status-connected';
|
|
110
|
+
connectBtn.disabled = true;
|
|
111
|
+
disconnectBtn.disabled = false;
|
|
112
|
+
} else {
|
|
113
|
+
badge.textContent = 'Disconnected';
|
|
114
|
+
badge.className = 'status-badge status-disconnected';
|
|
115
|
+
connectBtn.disabled = false;
|
|
116
|
+
disconnectBtn.disabled = true;
|
|
117
|
+
document.getElementById('clientId').textContent = 'Not connected';
|
|
118
|
+
document.getElementById('sessionId').textContent = 'Not joined';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function updatePeerList() {
|
|
123
|
+
const peerList = document.getElementById('peerList');
|
|
124
|
+
if (peerConnections.size === 0) {
|
|
125
|
+
peerList.innerHTML = '<p style="color: #94a3b8;">No peers connected</p>';
|
|
126
|
+
} else {
|
|
127
|
+
peerList.innerHTML = '';
|
|
128
|
+
peerConnections.forEach((pc, peerId) => {
|
|
129
|
+
const peerItem = document.createElement('div');
|
|
130
|
+
peerItem.className = 'peer-item';
|
|
131
|
+
peerItem.textContent = peerId.substring(0, 8) + '...';
|
|
132
|
+
peerList.appendChild(peerItem);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
window.connect = async function() {
|
|
138
|
+
const serverUrl = document.getElementById('serverUrl').value.trim();
|
|
139
|
+
const roomId = document.getElementById('roomId').value.trim();
|
|
140
|
+
|
|
141
|
+
if (!serverUrl) {
|
|
142
|
+
log('Please enter a server URL', 'error');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!roomId) {
|
|
147
|
+
log('Please enter a room ID', 'error');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
log(`Connecting to ${serverUrl}...`, 'info');
|
|
153
|
+
|
|
154
|
+
// For Cloudflare, use /ws endpoint with room ID query param
|
|
155
|
+
let finalUrl = serverUrl;
|
|
156
|
+
if (serverUrl.includes('signal.peer.ooo')) {
|
|
157
|
+
finalUrl = `${serverUrl.replace(/\/$/, '')}/ws?room=${roomId}`;
|
|
158
|
+
log(`Using Cloudflare Durable Objects with session: ${roomId}`, 'info');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
client = new UniWRTCClient(finalUrl, { autoReconnect: false });
|
|
162
|
+
|
|
163
|
+
client.on('connected', (data) => {
|
|
164
|
+
log(`Connected with client ID: ${data.clientId}`, 'success');
|
|
165
|
+
document.getElementById('clientId').textContent = data.clientId;
|
|
166
|
+
updateStatus(true);
|
|
167
|
+
|
|
168
|
+
// Auto-join the room
|
|
169
|
+
log(`Joining session: ${roomId}`, 'info');
|
|
170
|
+
client.joinSession(roomId);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
client.on('joined', (data) => {
|
|
174
|
+
log(`Joined session: ${data.sessionId}`, 'success');
|
|
175
|
+
document.getElementById('sessionId').textContent = data.sessionId;
|
|
176
|
+
|
|
177
|
+
if (data.clients && data.clients.length > 0) {
|
|
178
|
+
log(`Found ${data.clients.length} existing peers`, 'info');
|
|
179
|
+
data.clients.forEach(peerId => {
|
|
180
|
+
log(`Creating connection to existing peer: ${peerId.substring(0, 6)}...`, 'info');
|
|
181
|
+
createPeerConnection(peerId, true);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
client.on('peer-joined', (data) => {
|
|
187
|
+
// Only handle peers in our session
|
|
188
|
+
if (client.sessionId && data.sessionId !== client.sessionId) {
|
|
189
|
+
log(`Ignoring peer from different session: ${data.sessionId}`, 'warning');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
log(`Peer joined: ${data.peerId.substring(0, 6)}...`, 'success');
|
|
194
|
+
|
|
195
|
+
// Wait a bit to ensure both peers are ready
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
createPeerConnection(data.peerId, false);
|
|
198
|
+
}, 100);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
client.on('peer-left', (data) => {
|
|
202
|
+
log(`Peer left: ${data.peerId.substring(0, 6)}...`, 'warning');
|
|
203
|
+
const pc = peerConnections.get(data.peerId);
|
|
204
|
+
if (pc) {
|
|
205
|
+
pc.close();
|
|
206
|
+
peerConnections.delete(data.peerId);
|
|
207
|
+
dataChannels.delete(data.peerId);
|
|
208
|
+
updatePeerList();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
client.on('offer', async (data) => {
|
|
213
|
+
log(`Received offer from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
214
|
+
const pc = peerConnections.get(data.peerId) || await createPeerConnection(data.peerId, false);
|
|
215
|
+
await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
|
|
216
|
+
const answer = await pc.createAnswer();
|
|
217
|
+
await pc.setLocalDescription(answer);
|
|
218
|
+
client.sendAnswer(answer, data.peerId);
|
|
219
|
+
log(`Sent answer to ${data.peerId.substring(0, 6)}...`, 'success');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
client.on('answer', async (data) => {
|
|
223
|
+
log(`Received answer from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
224
|
+
const pc = peerConnections.get(data.peerId);
|
|
225
|
+
if (pc) {
|
|
226
|
+
await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
client.on('ice-candidate', async (data) => {
|
|
231
|
+
log(`Received ICE candidate from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
232
|
+
const pc = peerConnections.get(data.peerId);
|
|
233
|
+
if (pc && data.candidate) {
|
|
234
|
+
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
client.on('disconnected', () => {
|
|
239
|
+
log('Disconnected from server', 'error');
|
|
240
|
+
updateStatus(false);
|
|
241
|
+
peerConnections.forEach(pc => pc.close());
|
|
242
|
+
peerConnections.clear();
|
|
243
|
+
dataChannels.clear();
|
|
244
|
+
updatePeerList();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
client.on('error', (data) => {
|
|
248
|
+
log(`Error: ${data.message}`, 'error');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await client.connect();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
log(`Connection error: ${error.message}`, 'error');
|
|
254
|
+
updateStatus(false);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
window.disconnect = function() {
|
|
259
|
+
if (client) {
|
|
260
|
+
client.disconnect();
|
|
261
|
+
client = null;
|
|
262
|
+
peerConnections.forEach(pc => pc.close());
|
|
263
|
+
peerConnections.clear();
|
|
264
|
+
dataChannels.clear();
|
|
265
|
+
updatePeerList();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
async function createPeerConnection(peerId, shouldInitiate) {
|
|
270
|
+
if (peerConnections.has(peerId)) {
|
|
271
|
+
log(`Peer connection already exists for ${peerId.substring(0, 6)}...`, 'warning');
|
|
272
|
+
return peerConnections.get(peerId);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
log(`Creating peer connection with ${peerId.substring(0, 6)}... (shouldInitiate: ${shouldInitiate})`, 'info');
|
|
276
|
+
|
|
277
|
+
const pc = new RTCPeerConnection({
|
|
278
|
+
iceServers: [
|
|
279
|
+
{ urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] }
|
|
280
|
+
]
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
pc.onicecandidate = (event) => {
|
|
284
|
+
if (event.candidate) {
|
|
285
|
+
log(`Sending ICE candidate to ${peerId.substring(0, 6)}...`, 'info');
|
|
286
|
+
client.sendIceCandidate(event.candidate, peerId);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
pc.ondatachannel = (event) => {
|
|
291
|
+
log(`Received data channel from ${peerId.substring(0, 6)}`, 'info');
|
|
292
|
+
setupDataChannel(peerId, event.channel);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (shouldInitiate) {
|
|
296
|
+
const dc = pc.createDataChannel('chat');
|
|
297
|
+
setupDataChannel(peerId, dc);
|
|
298
|
+
|
|
299
|
+
const offer = await pc.createOffer();
|
|
300
|
+
await pc.setLocalDescription(offer);
|
|
301
|
+
client.sendOffer(offer, peerId);
|
|
302
|
+
log(`Sent offer to ${peerId.substring(0, 6)}...`, 'success');
|
|
303
|
+
} else {
|
|
304
|
+
log(`Waiting for offer from ${peerId.substring(0, 6)}...`, 'info');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
peerConnections.set(peerId, pc);
|
|
308
|
+
updatePeerList();
|
|
309
|
+
return pc;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function setupDataChannel(peerId, dataChannel) {
|
|
313
|
+
dataChannel.onopen = () => {
|
|
314
|
+
log(`Data channel open with ${peerId.substring(0, 6)}...`, 'success');
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
dataChannel.onmessage = (event) => {
|
|
318
|
+
displayChatMessage(event.data, `${peerId.substring(0, 6)}...`, false);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
dataChannel.onclose = () => {
|
|
322
|
+
log(`Data channel closed with ${peerId.substring(0, 6)}...`, 'warning');
|
|
323
|
+
dataChannels.delete(peerId);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
dataChannels.set(peerId, dataChannel);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
window.sendChatMessage = function() {
|
|
330
|
+
const message = document.getElementById('chatMessage').value.trim();
|
|
331
|
+
|
|
332
|
+
if (!message) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (dataChannels.size === 0) {
|
|
337
|
+
log('No peer connections available. Wait for data channels to open.', 'error');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Send to all connected peers
|
|
342
|
+
let sent = 0;
|
|
343
|
+
dataChannels.forEach((dc, peerId) => {
|
|
344
|
+
if (dc.readyState === 'open') {
|
|
345
|
+
dc.send(message);
|
|
346
|
+
sent++;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (sent > 0) {
|
|
351
|
+
displayChatMessage(message, 'You', true);
|
|
352
|
+
document.getElementById('chatMessage').value = '';
|
|
353
|
+
} else {
|
|
354
|
+
log('No open connections to send message', 'error');
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
function displayChatMessage(message, sender, isLocal) {
|
|
359
|
+
const chatContainer = document.getElementById('chatContainer');
|
|
360
|
+
|
|
361
|
+
// Clear placeholder if needed
|
|
362
|
+
if (chatContainer.querySelector('p')) {
|
|
363
|
+
chatContainer.innerHTML = '';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const messageEl = document.createElement('div');
|
|
367
|
+
messageEl.className = 'log-entry';
|
|
368
|
+
messageEl.style.background = isLocal ? 'rgba(16, 185, 129, 0.1)' : 'rgba(100, 116, 139, 0.1)';
|
|
369
|
+
messageEl.innerHTML = `
|
|
370
|
+
<span style="color: ${isLocal ? '#10b981' : '#64748b'}; font-weight: bold;">${sender}:</span>
|
|
371
|
+
<span style="margin-left: 8px;">${message}</span>
|
|
372
|
+
`;
|
|
373
|
+
chatContainer.appendChild(messageEl);
|
|
374
|
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Initialize
|
|
378
|
+
updateStatus(false);
|
|
379
|
+
log('UniWRTC Demo ready', 'success');
|
package/src/room.js
CHANGED
|
@@ -79,10 +79,14 @@ export class Room {
|
|
|
79
79
|
async handleJoin(clientId, message) {
|
|
80
80
|
const { sessionId, peerId } = message;
|
|
81
81
|
|
|
82
|
+
console.log(`[Room] Client ${clientId} joining session ${sessionId}`);
|
|
83
|
+
|
|
82
84
|
// Get list of other peers
|
|
83
85
|
const peers = Array.from(this.clients.keys())
|
|
84
86
|
.filter(id => id !== clientId);
|
|
85
87
|
|
|
88
|
+
console.log(`[Room] Existing peers in session:`, peers);
|
|
89
|
+
|
|
86
90
|
const client = this.clients.get(clientId);
|
|
87
91
|
if (client && client.readyState === WebSocket.OPEN) {
|
|
88
92
|
// Send joined confirmation (align with server schema)
|
|
@@ -95,6 +99,7 @@ export class Room {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// Notify other peers
|
|
102
|
+
console.log(`[Room] Broadcasting peer-joined for ${clientId}`);
|
|
98
103
|
this.broadcast({
|
|
99
104
|
type: 'peer-joined',
|
|
100
105
|
sessionId: sessionId,
|
package/src/style.css
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
9
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
10
|
+
min-height: 100vh;
|
|
11
|
+
padding: 20px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.container {
|
|
15
|
+
max-width: 1200px;
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.header {
|
|
20
|
+
text-align: center;
|
|
21
|
+
color: white;
|
|
22
|
+
margin-bottom: 30px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.header h1 {
|
|
26
|
+
font-size: 2.5em;
|
|
27
|
+
margin-bottom: 10px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.header p {
|
|
31
|
+
font-size: 1.1em;
|
|
32
|
+
opacity: 0.9;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.card {
|
|
36
|
+
background: white;
|
|
37
|
+
border-radius: 12px;
|
|
38
|
+
padding: 25px;
|
|
39
|
+
margin-bottom: 20px;
|
|
40
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.card h2 {
|
|
44
|
+
margin-bottom: 15px;
|
|
45
|
+
color: #333;
|
|
46
|
+
font-size: 1.5em;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.connection-controls {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: 1fr 1fr;
|
|
52
|
+
gap: 15px;
|
|
53
|
+
margin-bottom: 15px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
input {
|
|
57
|
+
width: 100%;
|
|
58
|
+
padding: 12px;
|
|
59
|
+
border: 2px solid #e0e0e0;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
font-size: 14px;
|
|
62
|
+
transition: border-color 0.3s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
input:focus {
|
|
66
|
+
outline: none;
|
|
67
|
+
border-color: #667eea;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
button {
|
|
71
|
+
padding: 12px 24px;
|
|
72
|
+
border: none;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
font-size: 14px;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
transition: all 0.3s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.btn-primary {
|
|
81
|
+
background: #667eea;
|
|
82
|
+
color: white;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.btn-primary:hover {
|
|
86
|
+
background: #5568d3;
|
|
87
|
+
transform: translateY(-2px);
|
|
88
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.btn-secondary {
|
|
92
|
+
background: #e0e0e0;
|
|
93
|
+
color: #333;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.btn-secondary:hover {
|
|
97
|
+
background: #d0d0d0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.btn-danger {
|
|
101
|
+
background: #ef4444;
|
|
102
|
+
color: white;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.btn-danger:hover {
|
|
106
|
+
background: #dc2626;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
button:disabled {
|
|
110
|
+
opacity: 0.5;
|
|
111
|
+
cursor: not-allowed;
|
|
112
|
+
transform: none !important;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.status-badge {
|
|
116
|
+
display: inline-block;
|
|
117
|
+
padding: 6px 12px;
|
|
118
|
+
border-radius: 20px;
|
|
119
|
+
font-size: 13px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.status-connected {
|
|
124
|
+
background: #10b981;
|
|
125
|
+
color: white;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.status-disconnected {
|
|
129
|
+
background: #ef4444;
|
|
130
|
+
color: white;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.log-container {
|
|
134
|
+
height: 250px;
|
|
135
|
+
overflow-y: auto;
|
|
136
|
+
background: #f9fafb;
|
|
137
|
+
border-radius: 6px;
|
|
138
|
+
padding: 15px;
|
|
139
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
140
|
+
font-size: 13px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.log-entry {
|
|
144
|
+
padding: 6px 10px;
|
|
145
|
+
margin-bottom: 4px;
|
|
146
|
+
border-radius: 4px;
|
|
147
|
+
background: rgba(100, 116, 139, 0.05);
|
|
148
|
+
line-height: 1.5;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.log-entry.success {
|
|
152
|
+
color: #059669;
|
|
153
|
+
background: rgba(16, 185, 129, 0.1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.log-entry.error {
|
|
157
|
+
color: #dc2626;
|
|
158
|
+
background: rgba(239, 68, 68, 0.1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.log-entry.info {
|
|
162
|
+
color: #2563eb;
|
|
163
|
+
background: rgba(37, 99, 235, 0.1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.log-entry.warning {
|
|
167
|
+
color: #d97706;
|
|
168
|
+
background: rgba(245, 158, 11, 0.1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.peer-list {
|
|
172
|
+
display: flex;
|
|
173
|
+
flex-wrap: wrap;
|
|
174
|
+
gap: 10px;
|
|
175
|
+
margin-top: 10px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.peer-item {
|
|
179
|
+
background: #f3f4f6;
|
|
180
|
+
padding: 8px 16px;
|
|
181
|
+
border-radius: 6px;
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
font-weight: 500;
|
|
184
|
+
color: #374151;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.chat-controls {
|
|
188
|
+
display: flex;
|
|
189
|
+
gap: 10px;
|
|
190
|
+
margin-top: 15px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.chat-controls input {
|
|
194
|
+
flex: 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#chatContainer {
|
|
198
|
+
height: 300px;
|
|
199
|
+
overflow-y: auto;
|
|
200
|
+
background: #f9fafb;
|
|
201
|
+
border-radius: 6px;
|
|
202
|
+
padding: 15px;
|
|
203
|
+
margin-bottom: 15px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#chatContainer p {
|
|
207
|
+
color: #64748b;
|
|
208
|
+
text-align: center;
|
|
209
|
+
padding: 20px;
|
|
210
|
+
}
|
package/vite.config.js
ADDED
package/wrangler.toml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
name = "uniwrtc"
|
|
2
2
|
main = "src/index.js"
|
|
3
3
|
compatibility_date = "2024-12-20"
|
|
4
|
+
workers_dev = false
|
|
4
5
|
|
|
5
6
|
[[migrations]]
|
|
6
7
|
tag = "v1"
|
|
@@ -19,5 +20,13 @@ routes = [
|
|
|
19
20
|
name = "ROOMS"
|
|
20
21
|
class_name = "Room"
|
|
21
22
|
|
|
23
|
+
[env.production.assets]
|
|
24
|
+
binding = "ASSETS"
|
|
25
|
+
directory = "dist"
|
|
26
|
+
|
|
22
27
|
[build]
|
|
23
|
-
command = "npm
|
|
28
|
+
command = "npm run build"
|
|
29
|
+
|
|
30
|
+
[build.upload]
|
|
31
|
+
format = "modules"
|
|
32
|
+
directory = "dist"
|