uniwrtc 1.0.0
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/.env.example +8 -0
- package/CLOUDFLARE_DEPLOYMENT.md +127 -0
- package/LICENSE +21 -0
- package/QUICKSTART_CLOUDFLARE.md +55 -0
- package/README.md +384 -0
- package/client-browser.js +249 -0
- package/client.js +231 -0
- package/demo.html +731 -0
- package/deploy-cloudflare.bat +113 -0
- package/deploy-cloudflare.sh +100 -0
- package/package-cf.json +16 -0
- package/package.json +26 -0
- package/server.js +350 -0
- package/src/client-cloudflare.js +247 -0
- package/src/index.js +35 -0
- package/src/room.js +211 -0
- package/test.js +62 -0
- package/wrangler.toml +23 -0
package/demo.html
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
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
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
16
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.container {
|
|
22
|
+
max-width: 1200px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.header {
|
|
27
|
+
text-align: center;
|
|
28
|
+
color: white;
|
|
29
|
+
margin-bottom: 30px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.header h1 {
|
|
33
|
+
font-size: 2.5em;
|
|
34
|
+
margin-bottom: 10px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.header p {
|
|
38
|
+
font-size: 1.1em;
|
|
39
|
+
opacity: 0.9;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.card {
|
|
43
|
+
background: white;
|
|
44
|
+
border-radius: 12px;
|
|
45
|
+
padding: 25px;
|
|
46
|
+
margin-bottom: 20px;
|
|
47
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.card h2 {
|
|
51
|
+
margin-bottom: 15px;
|
|
52
|
+
color: #333;
|
|
53
|
+
font-size: 1.5em;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.connection-controls {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: 1fr 1fr;
|
|
59
|
+
gap: 15px;
|
|
60
|
+
margin-bottom: 15px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
input {
|
|
64
|
+
width: 100%;
|
|
65
|
+
padding: 12px;
|
|
66
|
+
border: 2px solid #e0e0e0;
|
|
67
|
+
border-radius: 6px;
|
|
68
|
+
font-size: 14px;
|
|
69
|
+
transition: border-color 0.3s;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
input:focus {
|
|
73
|
+
outline: none;
|
|
74
|
+
border-color: #667eea;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
button {
|
|
78
|
+
padding: 12px 24px;
|
|
79
|
+
border: none;
|
|
80
|
+
border-radius: 6px;
|
|
81
|
+
font-size: 14px;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition: all 0.3s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.btn-primary {
|
|
88
|
+
background: #667eea;
|
|
89
|
+
color: white;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.btn-primary:hover {
|
|
93
|
+
background: #5568d3;
|
|
94
|
+
transform: translateY(-2px);
|
|
95
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.btn-secondary {
|
|
99
|
+
background: #e0e0e0;
|
|
100
|
+
color: #333;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.btn-secondary:hover {
|
|
104
|
+
background: #d0d0d0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.btn-danger {
|
|
108
|
+
background: #ef4444;
|
|
109
|
+
color: white;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.btn-danger:hover {
|
|
113
|
+
background: #dc2626;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.status-indicator {
|
|
117
|
+
display: inline-block;
|
|
118
|
+
width: 10px;
|
|
119
|
+
height: 10px;
|
|
120
|
+
border-radius: 50%;
|
|
121
|
+
margin-right: 8px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.status-indicator.connected {
|
|
125
|
+
background: #10b981;
|
|
126
|
+
box-shadow: 0 0 10px #10b981;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.status-indicator.disconnected {
|
|
130
|
+
background: #ef4444;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.status-bar {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
padding: 10px 15px;
|
|
137
|
+
background: #f5f5f5;
|
|
138
|
+
border-radius: 6px;
|
|
139
|
+
margin-bottom: 15px;
|
|
140
|
+
font-size: 14px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.log-container {
|
|
144
|
+
background: #1e1e1e;
|
|
145
|
+
color: #d4d4d4;
|
|
146
|
+
padding: 15px;
|
|
147
|
+
border-radius: 6px;
|
|
148
|
+
max-height: 300px;
|
|
149
|
+
overflow-y: auto;
|
|
150
|
+
font-family: 'Courier New', monospace;
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.log-entry {
|
|
155
|
+
padding: 4px 0;
|
|
156
|
+
border-bottom: 1px solid #2d2d2d;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.log-entry:last-child {
|
|
160
|
+
border-bottom: none;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.log-timestamp {
|
|
164
|
+
color: #858585;
|
|
165
|
+
margin-right: 10px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.log-type-info {
|
|
169
|
+
color: #4fc3f7;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.log-type-success {
|
|
173
|
+
color: #81c784;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.log-type-error {
|
|
177
|
+
color: #e57373;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.log-type-warning {
|
|
181
|
+
color: #ffb74d;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.peers-list {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-wrap: wrap;
|
|
187
|
+
gap: 10px;
|
|
188
|
+
margin-top: 15px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.peer-item {
|
|
192
|
+
background: #f5f5f5;
|
|
193
|
+
padding: 10px 15px;
|
|
194
|
+
border-radius: 6px;
|
|
195
|
+
font-size: 14px;
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: 8px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.peer-status {
|
|
202
|
+
width: 8px;
|
|
203
|
+
height: 8px;
|
|
204
|
+
border-radius: 50%;
|
|
205
|
+
background: #10b981;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.video-container {
|
|
209
|
+
display: grid;
|
|
210
|
+
grid-template-columns: 1fr 1fr;
|
|
211
|
+
gap: 15px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.video-wrapper {
|
|
215
|
+
position: relative;
|
|
216
|
+
background: #000;
|
|
217
|
+
border-radius: 8px;
|
|
218
|
+
overflow: hidden;
|
|
219
|
+
aspect-ratio: 16/9;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
video {
|
|
223
|
+
width: 100%;
|
|
224
|
+
height: 100%;
|
|
225
|
+
object-fit: cover;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.video-label {
|
|
229
|
+
position: absolute;
|
|
230
|
+
top: 10px;
|
|
231
|
+
left: 10px;
|
|
232
|
+
background: rgba(0, 0, 0, 0.7);
|
|
233
|
+
color: white;
|
|
234
|
+
padding: 5px 10px;
|
|
235
|
+
border-radius: 4px;
|
|
236
|
+
font-size: 12px;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@media (max-width: 768px) {
|
|
241
|
+
.connection-controls {
|
|
242
|
+
grid-template-columns: 1fr;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.video-container {
|
|
246
|
+
grid-template-columns: 1fr;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
</style>
|
|
250
|
+
</head>
|
|
251
|
+
<body>
|
|
252
|
+
<div class="container">
|
|
253
|
+
<div class="header">
|
|
254
|
+
<h1>🌐 UniWRTC Demo</h1>
|
|
255
|
+
<p>Universal WebRTC Signaling Service</p>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="card">
|
|
259
|
+
<h2>Connection Control</h2>
|
|
260
|
+
|
|
261
|
+
<div class="status-bar">
|
|
262
|
+
<span class="status-indicator disconnected" id="statusIndicator"></span>
|
|
263
|
+
<span id="statusText">Disconnected</span>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="status-bar" id="myPeerIdBar" style="display: none; background: #e8f5e9; color: #2e7d32; font-weight: bold;">
|
|
267
|
+
<span>Your Peer ID: </span>
|
|
268
|
+
<span id="myPeerId" style="user-select: all; cursor: text;"></span>
|
|
269
|
+
<button class="btn-secondary" onclick="copyMyPeerId()" style="margin-left: 10px; padding: 6px 12px;">Copy</button>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div class="connection-controls">
|
|
273
|
+
<input type="text" id="serverUrl" placeholder="wss://signal.peer.ooo" value="wss://signal.peer.ooo">
|
|
274
|
+
<input type="text" id="customPeerId" placeholder="Custom Peer ID (optional)">
|
|
275
|
+
<button class="btn-primary" id="connectBtn" onclick="connect()">Connect</button>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div style="margin-top: 15px;">
|
|
279
|
+
<button class="btn-secondary" onclick="listRooms()">List Rooms</button>
|
|
280
|
+
<button class="btn-danger" onclick="disconnect()">Disconnect</button>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="card">
|
|
285
|
+
<h2>Room Status</h2>
|
|
286
|
+
|
|
287
|
+
<div class="connection-controls" style="margin-bottom: 15px;">
|
|
288
|
+
<input type="text" id="roomId" placeholder="Enter room ID" value="demo-room">
|
|
289
|
+
<button class="btn-primary" id="joinBtn" onclick="joinRoom()" disabled>Join Room</button>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<div id="roomInfo">
|
|
293
|
+
<p style="color: #666;">Not in a room</p>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div style="margin-top: 15px; margin-bottom: 15px;">
|
|
297
|
+
<h3 style="font-size: 14px; margin-bottom: 10px; color: #666;">Connected Peers</h3>
|
|
298
|
+
<div class="peers-list" id="peersList"></div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div style="margin-top: 15px;">
|
|
302
|
+
<h3 style="font-size: 14px; margin-bottom: 10px; color: #666;">Connect to Specific Peer</h3>
|
|
303
|
+
<p style="font-size: 12px; color: #999; margin-bottom: 8px;">Enter a peer's ID to establish direct P2P connection</p>
|
|
304
|
+
<div class="connection-controls">
|
|
305
|
+
<input type="text" id="manualPeerId" placeholder="Enter peer ID">
|
|
306
|
+
<button class="btn-primary" onclick="connectToPeer()">Connect to Peer</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div class="card">
|
|
312
|
+
<h2>Chat</h2>
|
|
313
|
+
<div class="log-container" id="chatContainer" style="max-height: 250px; margin-bottom: 15px;">
|
|
314
|
+
<p style="color: #999; text-align: center; padding: 20px;">Messages will appear here</p>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="connection-controls" style="margin-bottom: 0;">
|
|
317
|
+
<input type="text" id="chatMessage" placeholder="Type a message..." style="grid-column: 1 / -1;">
|
|
318
|
+
<button class="btn-primary" onclick="sendChatMessage()" style="grid-column: 1 / -1;">Send Message</button>
|
|
319
|
+
</div>
|
|
320
|
+
</div> <div class="card">
|
|
321
|
+
<h2>Activity Log</h2>
|
|
322
|
+
<div class="log-container" id="logContainer">
|
|
323
|
+
<div class="log-entry">
|
|
324
|
+
<span class="log-timestamp">Ready</span>
|
|
325
|
+
<span class="log-type-info">Waiting for connection...</span>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<script src="client-browser.js"></script>
|
|
332
|
+
<script>
|
|
333
|
+
let client = null;
|
|
334
|
+
let peerConnections = new Map(); // Map of peerId -> RTCPeerConnection
|
|
335
|
+
let dataChannels = new Map(); // Map of peerId -> RTCDataChannel
|
|
336
|
+
const logContainer = document.getElementById('logContainer');
|
|
337
|
+
const statusIndicator = document.getElementById('statusIndicator');
|
|
338
|
+
const statusText = document.getElementById('statusText');
|
|
339
|
+
const connectBtn = document.getElementById('connectBtn');
|
|
340
|
+
const joinBtn = document.getElementById('joinBtn');
|
|
341
|
+
const roomInfo = document.getElementById('roomInfo');
|
|
342
|
+
const peersList = document.getElementById('peersList');
|
|
343
|
+
|
|
344
|
+
function log(message, type = 'info') {
|
|
345
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
346
|
+
const entry = document.createElement('div');
|
|
347
|
+
entry.className = 'log-entry';
|
|
348
|
+
entry.innerHTML = `
|
|
349
|
+
<span class="log-timestamp">[${timestamp}]</span>
|
|
350
|
+
<span class="log-type-${type}">${message}</span>
|
|
351
|
+
`;
|
|
352
|
+
logContainer.appendChild(entry);
|
|
353
|
+
logContainer.scrollTop = logContainer.scrollHeight;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function updateStatus(connected) {
|
|
357
|
+
const myPeerIdBar = document.getElementById('myPeerIdBar');
|
|
358
|
+
const myPeerId = document.getElementById('myPeerId');
|
|
359
|
+
|
|
360
|
+
if (connected) {
|
|
361
|
+
statusIndicator.className = 'status-indicator connected';
|
|
362
|
+
statusText.textContent = `Connected`;
|
|
363
|
+
connectBtn.textContent = 'Connected';
|
|
364
|
+
connectBtn.disabled = true;
|
|
365
|
+
joinBtn.disabled = false;
|
|
366
|
+
|
|
367
|
+
// Show peer ID
|
|
368
|
+
myPeerId.textContent = client.clientId;
|
|
369
|
+
myPeerIdBar.style.display = 'flex';
|
|
370
|
+
} else {
|
|
371
|
+
statusIndicator.className = 'status-indicator disconnected';
|
|
372
|
+
statusText.textContent = 'Disconnected';
|
|
373
|
+
connectBtn.textContent = 'Connect';
|
|
374
|
+
connectBtn.disabled = false;
|
|
375
|
+
joinBtn.disabled = true;
|
|
376
|
+
roomInfo.innerHTML = '<p style="color: #666;">Not in a room</p>';
|
|
377
|
+
peersList.innerHTML = '';
|
|
378
|
+
|
|
379
|
+
// Hide peer ID
|
|
380
|
+
myPeerIdBar.style.display = 'none';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function copyMyPeerId() {
|
|
385
|
+
const peerId = document.getElementById('myPeerId').textContent;
|
|
386
|
+
navigator.clipboard.writeText(peerId);
|
|
387
|
+
log('Copied your peer ID to clipboard', 'success');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function updateRoomInfo(roomId, clientId) {
|
|
391
|
+
const peerCount = peersList.children.length;
|
|
392
|
+
roomInfo.innerHTML = `
|
|
393
|
+
<p><strong>Room:</strong> ${roomId}</p>
|
|
394
|
+
<p><strong>Your ID:</strong> ${clientId}</p>
|
|
395
|
+
<p><strong>Peers in room:</strong> ${peerCount}</p>
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function updatePeersList(peers) {
|
|
400
|
+
peersList.innerHTML = '';
|
|
401
|
+
if (peers.length === 0) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
peers.forEach(peerId => {
|
|
405
|
+
const peerItem = document.createElement('div');
|
|
406
|
+
peerItem.className = 'peer-item';
|
|
407
|
+
peerItem.style.cursor = 'pointer';
|
|
408
|
+
peerItem.title = 'Click to copy peer ID';
|
|
409
|
+
peerItem.innerHTML = `
|
|
410
|
+
<span class="peer-status"></span>
|
|
411
|
+
<span>${peerId}</span>
|
|
412
|
+
`;
|
|
413
|
+
peerItem.onclick = () => {
|
|
414
|
+
navigator.clipboard.writeText(peerId);
|
|
415
|
+
log(`Copied peer ID: ${peerId}`, 'success');
|
|
416
|
+
};
|
|
417
|
+
peersList.appendChild(peerItem);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function connect() {
|
|
422
|
+
const serverUrl = document.getElementById('serverUrl').value;
|
|
423
|
+
|
|
424
|
+
if (!serverUrl) {
|
|
425
|
+
log('Please enter a server URL', 'error');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
log('Connecting to signaling server...', 'info');
|
|
431
|
+
|
|
432
|
+
const customPeerId = document.getElementById('customPeerId').value.trim();
|
|
433
|
+
client = new UniWRTCClient(serverUrl, { customPeerId: customPeerId || null });
|
|
434
|
+
|
|
435
|
+
client.on('connected', (data) => {
|
|
436
|
+
log(`Connected with ID: ${data.clientId}`, 'success');
|
|
437
|
+
updateStatus(true);
|
|
438
|
+
|
|
439
|
+
// Auto-join room after connecting
|
|
440
|
+
const roomId = document.getElementById('roomId').value;
|
|
441
|
+
if (roomId) {
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
log(`Auto-joining room: ${roomId}...`, 'info');
|
|
444
|
+
client.joinRoom(roomId);
|
|
445
|
+
}, 500);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
client.on('disconnected', () => {
|
|
450
|
+
log('Disconnected from server', 'warning');
|
|
451
|
+
updateStatus(false);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Track known peers
|
|
455
|
+
let knownPeers = [];
|
|
456
|
+
|
|
457
|
+
client.on('joined', (data) => {
|
|
458
|
+
log(`Joined room: ${data.roomId}`, 'success');
|
|
459
|
+
knownPeers = data.clients || [];
|
|
460
|
+
updatePeersList(knownPeers);
|
|
461
|
+
|
|
462
|
+
// Update room info with dynamic peer count
|
|
463
|
+
updateRoomInfo(data.roomId, data.peerId || client.clientId);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
client.on('peer-joined', (data) => {
|
|
467
|
+
log(`Peer joined: ${data.peerId}`, 'success');
|
|
468
|
+
// Add to known peers if not already there
|
|
469
|
+
if (data.peerId && !knownPeers.includes(data.peerId)) {
|
|
470
|
+
knownPeers.push(data.peerId);
|
|
471
|
+
updatePeersList(knownPeers);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Update room info with new peer count
|
|
475
|
+
updateRoomInfo(client.roomId, client.clientId);
|
|
476
|
+
|
|
477
|
+
// Auto-initiate P2P connection
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
createPeerConnection(data.peerId, true);
|
|
480
|
+
}, 500);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
client.on('peer-left', (data) => {
|
|
484
|
+
const peerId = data.peerId || data.clientId;
|
|
485
|
+
log(`Peer left: ${peerId}`, 'warning');
|
|
486
|
+
// Remove from known peers
|
|
487
|
+
knownPeers = knownPeers.filter(id => id !== peerId);
|
|
488
|
+
updatePeersList(knownPeers);
|
|
489
|
+
|
|
490
|
+
// Update room info with new peer count
|
|
491
|
+
updateRoomInfo(client.roomId, client.clientId);
|
|
492
|
+
|
|
493
|
+
// Close peer connection
|
|
494
|
+
const pc = peerConnections.get(peerId);
|
|
495
|
+
if (pc) {
|
|
496
|
+
pc.close();
|
|
497
|
+
peerConnections.delete(peerId);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
client.on('offer', async (data) => {
|
|
502
|
+
log(`Received offer from: ${data.peerId}`, 'info');
|
|
503
|
+
try {
|
|
504
|
+
const pc = await createPeerConnection(data.peerId, false);
|
|
505
|
+
|
|
506
|
+
// Set remote description first
|
|
507
|
+
await pc.setRemoteDescription(data.offer);
|
|
508
|
+
|
|
509
|
+
// Then create and set local answer
|
|
510
|
+
const answer = await pc.createAnswer();
|
|
511
|
+
await pc.setLocalDescription(answer);
|
|
512
|
+
|
|
513
|
+
// Send answer back
|
|
514
|
+
client.sendAnswer(data.peerId, answer);
|
|
515
|
+
} catch (e) {
|
|
516
|
+
log(`Offer error: ${e.message}`, 'error');
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
client.on('answer', async (data) => {
|
|
521
|
+
log(`Received answer from: ${data.peerId}`, 'info');
|
|
522
|
+
const pc = peerConnections.get(data.peerId);
|
|
523
|
+
if (pc) {
|
|
524
|
+
try {
|
|
525
|
+
await pc.setRemoteDescription(data.answer);
|
|
526
|
+
} catch (e) {
|
|
527
|
+
log(`Answer error: ${e.message}`, 'error');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
client.on('ice-candidate', async (data) => {
|
|
533
|
+
const pc = peerConnections.get(data.peerId);
|
|
534
|
+
if (pc && data.candidate) {
|
|
535
|
+
try {
|
|
536
|
+
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
|
|
537
|
+
} catch (e) {
|
|
538
|
+
log(`ICE error: ${e.message}`, 'warning');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
client.on('room-list', (data) => {
|
|
544
|
+
log(`Available rooms: ${data.rooms.length}`, 'info');
|
|
545
|
+
data.rooms.forEach(room => {
|
|
546
|
+
log(` - ${room.id} (${room.clients} clients)`, 'info');
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
client.on('error', (data) => {
|
|
551
|
+
log(`Error: ${data.message}`, 'error');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
client.on('chat', (data) => {
|
|
555
|
+
displayChatMessage(data.text, `${data.senderId.substring(0, 6)}...`, false);
|
|
556
|
+
log(`Chat from ${data.senderId}: ${data.text}`, 'info');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await client.connect();
|
|
560
|
+
} catch (error) {
|
|
561
|
+
log(`Connection failed: ${error.message}`, 'error');
|
|
562
|
+
updateStatus(false);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function disconnect() {
|
|
567
|
+
if (client) {
|
|
568
|
+
client.disconnect();
|
|
569
|
+
client = null;
|
|
570
|
+
log('Disconnected', 'warning');
|
|
571
|
+
updateStatus(false);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function connectToPeer() {
|
|
576
|
+
const peerId = document.getElementById('manualPeerId').value.trim();
|
|
577
|
+
|
|
578
|
+
if (!peerId) {
|
|
579
|
+
log('Please enter a peer ID', 'error');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (!client || !client.roomId) {
|
|
584
|
+
log('Not connected to a room', 'error');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
log(`Manually connecting to peer: ${peerId}...`, 'info');
|
|
589
|
+
createPeerConnection(peerId, true);
|
|
590
|
+
document.getElementById('manualPeerId').value = '';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function joinRoom() {
|
|
594
|
+
const roomId = document.getElementById('roomId').value;
|
|
595
|
+
|
|
596
|
+
if (!roomId) {
|
|
597
|
+
log('Please enter a room ID', 'error');
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!client) {
|
|
602
|
+
log('Not connected to server', 'error');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
log(`Joining room: ${roomId}...`, 'info');
|
|
607
|
+
client.joinRoom(roomId);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function listRooms() {
|
|
611
|
+
if (!client) {
|
|
612
|
+
log('Not connected to server', 'error');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
log('Requesting room list...', 'info');
|
|
617
|
+
client.listRooms();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function createPeerConnection(peerId, initiator = false) {
|
|
621
|
+
if (peerConnections.has(peerId)) {
|
|
622
|
+
return peerConnections.get(peerId);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Determine who should initiate based on peer IDs (lexicographic comparison)
|
|
626
|
+
// Only the peer with smaller ID creates the offer
|
|
627
|
+
const shouldInitiate = client.clientId < peerId;
|
|
628
|
+
|
|
629
|
+
const pc = new RTCPeerConnection({
|
|
630
|
+
iceServers: [
|
|
631
|
+
{ urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] }
|
|
632
|
+
]
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
pc.onicecandidate = (event) => {
|
|
636
|
+
if (event.candidate) {
|
|
637
|
+
log(`Sending ICE candidate to ${peerId.substring(0, 6)}...`, 'info');
|
|
638
|
+
client.sendIceCandidate(peerId, event.candidate);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
pc.ondatachannel = (event) => {
|
|
643
|
+
setupDataChannel(peerId, event.channel);
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
if (shouldInitiate) {
|
|
647
|
+
const dc = pc.createDataChannel('chat');
|
|
648
|
+
setupDataChannel(peerId, dc);
|
|
649
|
+
|
|
650
|
+
const offer = await pc.createOffer();
|
|
651
|
+
await pc.setLocalDescription(offer);
|
|
652
|
+
client.sendOffer(peerId, offer);
|
|
653
|
+
log(`Sent offer to ${peerId.substring(0, 6)}...`, 'success');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
peerConnections.set(peerId, pc);
|
|
657
|
+
return pc;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function setupDataChannel(peerId, dataChannel) {
|
|
661
|
+
dataChannel.onopen = () => {
|
|
662
|
+
log(`Data channel open with ${peerId.substring(0, 6)}...`, 'success');
|
|
663
|
+
dataChannels.set(peerId, dataChannel);
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
dataChannel.onmessage = (event) => {
|
|
667
|
+
displayChatMessage(event.data, `${peerId.substring(0, 6)}...`, false);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
dataChannel.onclose = () => {
|
|
671
|
+
log(`Data channel closed with ${peerId.substring(0, 6)}...`, 'warning');
|
|
672
|
+
dataChannels.delete(peerId);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
dataChannels.set(peerId, dataChannel);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function sendChatMessage() {
|
|
679
|
+
const message = document.getElementById('chatMessage').value.trim();
|
|
680
|
+
|
|
681
|
+
if (!message) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (dataChannels.size === 0) {
|
|
686
|
+
log('No peer connections available. Wait for data channels to open.', 'error');
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Send to all connected peers
|
|
691
|
+
let sent = 0;
|
|
692
|
+
dataChannels.forEach((dc, peerId) => {
|
|
693
|
+
if (dc.readyState === 'open') {
|
|
694
|
+
dc.send(message);
|
|
695
|
+
sent++;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
if (sent > 0) {
|
|
700
|
+
displayChatMessage(message, 'You', true);
|
|
701
|
+
document.getElementById('chatMessage').value = '';
|
|
702
|
+
} else {
|
|
703
|
+
log('No open connections to send message', 'error');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function displayChatMessage(message, sender, isLocal) {
|
|
708
|
+
const chatContainer = document.getElementById('chatContainer');
|
|
709
|
+
|
|
710
|
+
// Clear placeholder if needed
|
|
711
|
+
if (chatContainer.querySelector('p')) {
|
|
712
|
+
chatContainer.innerHTML = '';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const messageEl = document.createElement('div');
|
|
716
|
+
messageEl.className = 'log-entry';
|
|
717
|
+
messageEl.style.background = isLocal ? 'rgba(16, 185, 129, 0.1)' : 'rgba(100, 116, 139, 0.1)';
|
|
718
|
+
messageEl.innerHTML = `
|
|
719
|
+
<span style="color: ${isLocal ? '#10b981' : '#64748b'}; font-weight: bold;">${sender}:</span>
|
|
720
|
+
<span style="margin-left: 8px;">${message}</span>
|
|
721
|
+
`;
|
|
722
|
+
chatContainer.appendChild(messageEl);
|
|
723
|
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Initialize
|
|
727
|
+
updateStatus(false);
|
|
728
|
+
log('UniWRTC Demo ready', 'success');
|
|
729
|
+
</script>
|
|
730
|
+
</body>
|
|
731
|
+
</html>
|