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 CHANGED
@@ -53,14 +53,21 @@ PORT=8080
53
53
 
54
54
  ### Try the Demo
55
55
 
56
- Open `demo.html` in your web browser to try the interactive demo:
57
-
58
- 1. Start the server with `npm start` (local signaling at `ws://localhost:8080`), **or** use the deployed Workers endpoint `wss://signal.peer.ooo`.
59
- 2. Open `demo.html` in your browser.
60
- 3. Click "Connect" to connect to the signaling server.
61
- 4. Enter a session ID and click "Join Session".
62
- 5. Open another browser window/tab with the same demo page.
63
- 6. Join the same session to see peer connections in action and P2P data channels open.
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 { UniWRTCClient } from 'uniwrtc/client-browser.js';
171
- // or CommonJS (Node)
172
- const { UniWRTCClient } = require('uniwrtc/client.js');
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 room
215
- client.joinRoom('my-room');
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 rooms
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 Management
375
+ ### Session-based Peer Isolation
363
376
 
364
- - Each session is identified by a unique session ID (string)
365
- - Clients can join/leave sessions dynamically
366
- - Messages can be sent to specific peers or broadcast to all peers in a session
367
- - Empty sessions are automatically cleaned up
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 joins a room
374
- 4. Server notifies existing peers about the new client
375
- 5. Peers exchange WebRTC signaling messages through the server
376
- 6. Server routes messages based on target ID or broadcasts to room
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",
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": "node server.js",
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 signaling
22
- if (url.pathname === '/signaling' || url.pathname === '/') {
23
- const roomId = url.searchParams.get('room') || 'default';
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
- // Get Durable Object stub for this room
26
- const id = env.ROOMS.idFromName(roomId);
27
- const roomStub = env.ROOMS.get(id);
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
- // Forward request to Durable Object
30
- return roomStub.fetch(request);
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
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ root: '.',
5
+ build: {
6
+ outDir: 'dist',
7
+ rollupOptions: {
8
+ input: {
9
+ main: './index.html'
10
+ }
11
+ }
12
+ }
13
+ });
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 install"
28
+ command = "npm run build"
29
+
30
+ [build.upload]
31
+ format = "modules"
32
+ directory = "dist"