uniwrtc 1.0.8 → 1.0.9
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/client-browser.js +254 -2
- package/client.js +136 -178
- package/package.json +1 -1
- package/src/client-cloudflare.js +119 -138
- package/src/main.js +15 -55
- package/simple-peer-adapter.js +0 -124
package/client-browser.js
CHANGED
|
@@ -1,13 +1,265 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UniWRTC Client - WebRTC Signaling Client Library
|
|
3
|
-
* Browser version
|
|
3
|
+
* Browser-only version
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
class UniWRTCClient {
|
|
7
|
+
constructor(serverUrl, options = {}) {
|
|
8
|
+
this.serverUrl = serverUrl;
|
|
9
|
+
this.ws = null;
|
|
10
|
+
this.clientId = null;
|
|
11
|
+
this.sessionId = null;
|
|
12
|
+
this.peers = new Map();
|
|
13
|
+
this._connectedOnce = false;
|
|
14
|
+
this.options = {
|
|
15
|
+
autoReconnect: true,
|
|
16
|
+
reconnectDelay: 3000,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
this.eventHandlers = {
|
|
20
|
+
'connected': [],
|
|
21
|
+
'disconnected': [],
|
|
22
|
+
'joined': [],
|
|
23
|
+
'peer-joined': [],
|
|
24
|
+
'peer-left': [],
|
|
25
|
+
'offer': [],
|
|
26
|
+
'answer': [],
|
|
27
|
+
'ice-candidate': [],
|
|
28
|
+
'room-list': [],
|
|
29
|
+
'error': []
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connect() {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
try {
|
|
36
|
+
this.ws = new WebSocket(this.serverUrl);
|
|
37
|
+
|
|
38
|
+
this.ws.onopen = () => {
|
|
39
|
+
console.log('Connected to signaling server');
|
|
40
|
+
|
|
41
|
+
// Send custom peer ID if provided
|
|
42
|
+
if (this.options.customPeerId) {
|
|
43
|
+
this.send({
|
|
44
|
+
type: 'set-id',
|
|
45
|
+
customId: this.options.customPeerId
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.ws.onmessage = (event) => {
|
|
51
|
+
try {
|
|
52
|
+
const message = JSON.parse(event.data);
|
|
53
|
+
this.handleMessage(message);
|
|
54
|
+
|
|
55
|
+
if (message.type === 'welcome' && !this._connectedOnce) {
|
|
56
|
+
this.clientId = message.clientId;
|
|
57
|
+
this._connectedOnce = true;
|
|
58
|
+
this.emit('connected', { clientId: this.clientId });
|
|
59
|
+
resolve(this.clientId);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Error parsing message:', error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.ws.onclose = () => {
|
|
67
|
+
console.log('Disconnected from signaling server');
|
|
68
|
+
this.emit('disconnected');
|
|
69
|
+
|
|
70
|
+
if (this.options.autoReconnect) {
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
console.log('Attempting to reconnect...');
|
|
73
|
+
this.connect();
|
|
74
|
+
}, this.options.reconnectDelay);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.ws.onerror = (error) => {
|
|
79
|
+
console.error('WebSocket error:', error);
|
|
80
|
+
reject(error);
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
reject(error);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
disconnect() {
|
|
89
|
+
if (this.ws) {
|
|
90
|
+
this.options.autoReconnect = false;
|
|
91
|
+
this.ws.close();
|
|
92
|
+
this.ws = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
joinSession(sessionId) {
|
|
97
|
+
// Prevent duplicate join calls for the same session
|
|
98
|
+
if (this.sessionId === sessionId) return;
|
|
99
|
+
this.sessionId = sessionId;
|
|
100
|
+
|
|
101
|
+
// Send join message
|
|
102
|
+
this.send({
|
|
103
|
+
type: 'join',
|
|
104
|
+
sessionId: sessionId,
|
|
105
|
+
peerId: this.clientId
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
leaveSession() {
|
|
110
|
+
if (this.sessionId) {
|
|
111
|
+
this.send({
|
|
112
|
+
type: 'leave',
|
|
113
|
+
sessionId: this.sessionId
|
|
114
|
+
});
|
|
115
|
+
this.sessionId = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
send(message) {
|
|
120
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
121
|
+
this.ws.send(JSON.stringify(message));
|
|
122
|
+
} else {
|
|
123
|
+
console.warn('WebSocket is not connected');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
sendOffer(offer, targetId) {
|
|
128
|
+
console.log(`[Client] Sending offer to ${targetId}`);
|
|
129
|
+
this.send({
|
|
130
|
+
type: 'offer',
|
|
131
|
+
offer: offer,
|
|
132
|
+
targetId: targetId,
|
|
133
|
+
sessionId: this.sessionId
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sendAnswer(answer, targetId) {
|
|
138
|
+
console.log(`[Client] Sending answer to ${targetId}`);
|
|
139
|
+
this.send({
|
|
140
|
+
type: 'answer',
|
|
141
|
+
answer: answer,
|
|
142
|
+
targetId: targetId,
|
|
143
|
+
sessionId: this.sessionId
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
sendIceCandidate(candidate, targetId) {
|
|
148
|
+
console.log(`[Client] Sending ICE candidate to ${targetId}`);
|
|
149
|
+
this.send({
|
|
150
|
+
type: 'ice-candidate',
|
|
151
|
+
candidate: candidate,
|
|
152
|
+
targetId: targetId,
|
|
153
|
+
sessionId: this.sessionId
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
listRooms() {
|
|
158
|
+
this.send({
|
|
159
|
+
type: 'list-rooms'
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
on(event, handler) {
|
|
164
|
+
if (this.eventHandlers[event]) {
|
|
165
|
+
this.eventHandlers[event].push(handler);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
off(event, handler) {
|
|
170
|
+
if (this.eventHandlers[event]) {
|
|
171
|
+
this.eventHandlers[event] = this.eventHandlers[event].filter(h => h !== handler);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
emit(event, data) {
|
|
176
|
+
if (this.eventHandlers[event]) {
|
|
177
|
+
this.eventHandlers[event].forEach(handler => {
|
|
178
|
+
try {
|
|
179
|
+
handler(data);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`Error in ${event} handler:`, error);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
handleMessage(message) {
|
|
188
|
+
switch (message.type) {
|
|
189
|
+
case 'welcome':
|
|
190
|
+
// Only set clientId here; 'connected' is emitted in connect() with a guard
|
|
191
|
+
this.clientId = message.clientId;
|
|
192
|
+
console.log('[UniWRTC] If this helps, consider donating ❤️ → https://coff.ee/draederg');
|
|
193
|
+
break;
|
|
194
|
+
case 'joined':
|
|
195
|
+
this.sessionId = message.sessionId;
|
|
196
|
+
this.emit('joined', {
|
|
197
|
+
sessionId: message.sessionId,
|
|
198
|
+
peerId: message.peerId,
|
|
199
|
+
clientId: message.clientId,
|
|
200
|
+
clients: message.clients
|
|
201
|
+
});
|
|
202
|
+
break;
|
|
203
|
+
case 'peer-joined':
|
|
204
|
+
this.emit('peer-joined', {
|
|
205
|
+
sessionId: message.sessionId,
|
|
206
|
+
peerId: message.peerId
|
|
207
|
+
});
|
|
208
|
+
break;
|
|
209
|
+
case 'peer-left':
|
|
210
|
+
this.emit('peer-left', {
|
|
211
|
+
sessionId: message.sessionId,
|
|
212
|
+
peerId: message.peerId
|
|
213
|
+
});
|
|
214
|
+
break;
|
|
215
|
+
case 'offer':
|
|
216
|
+
console.log(`[Client] Received offer from ${message.peerId}`);
|
|
217
|
+
this.emit('offer', {
|
|
218
|
+
peerId: message.peerId,
|
|
219
|
+
offer: message.offer
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
case 'answer':
|
|
223
|
+
console.log(`[Client] Received answer from ${message.peerId}`);
|
|
224
|
+
this.emit('answer', {
|
|
225
|
+
peerId: message.peerId,
|
|
226
|
+
answer: message.answer
|
|
227
|
+
});
|
|
228
|
+
break;
|
|
229
|
+
case 'ice-candidate':
|
|
230
|
+
console.log(`[Client] Received ICE candidate from ${message.peerId}`);
|
|
231
|
+
this.emit('ice-candidate', {
|
|
232
|
+
peerId: message.peerId,
|
|
233
|
+
candidate: message.candidate
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
case 'room-list':
|
|
237
|
+
this.emit('room-list', {
|
|
238
|
+
rooms: message.rooms
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
case 'error':
|
|
242
|
+
this.emit('error', {
|
|
243
|
+
message: message.message
|
|
244
|
+
});
|
|
245
|
+
break;
|
|
246
|
+
case 'chat':
|
|
247
|
+
this.emit('chat', {
|
|
248
|
+
text: message.text,
|
|
249
|
+
peerId: message.peerId,
|
|
250
|
+
sessionId: message.sessionId
|
|
251
|
+
});
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
console.log('Unknown message type:', message.type);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
7
258
|
|
|
8
259
|
// Attach to window for non-module script usage
|
|
9
260
|
if (typeof window !== 'undefined') {
|
|
10
261
|
window.UniWRTCClient = UniWRTCClient;
|
|
11
262
|
}
|
|
12
263
|
|
|
264
|
+
// ESM default export for bundlers like Vite
|
|
13
265
|
export default UniWRTCClient;
|
package/client.js
CHANGED
|
@@ -1,69 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* UniWRTC Client -
|
|
2
|
+
* UniWRTC Client - WebRTC Signaling Client Library
|
|
3
|
+
* Simplifies connection to the UniWRTC signaling server
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
import https from 'https';
|
|
7
|
-
|
|
8
|
-
function requestJson(urlString, { method, body } = {}) {
|
|
9
|
-
return new Promise((resolve, reject) => {
|
|
10
|
-
const url = new URL(urlString);
|
|
11
|
-
const isHttps = url.protocol === 'https:';
|
|
12
|
-
const lib = isHttps ? https : http;
|
|
13
|
-
|
|
14
|
-
const payload = body === undefined ? null : JSON.stringify(body);
|
|
15
|
-
const req = lib.request(
|
|
16
|
-
{
|
|
17
|
-
hostname: url.hostname,
|
|
18
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
19
|
-
path: url.pathname + url.search,
|
|
20
|
-
method: method || 'GET',
|
|
21
|
-
headers: {
|
|
22
|
-
...(payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
23
|
-
'Accept': 'application/json'
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
(res) => {
|
|
27
|
-
let data = '';
|
|
28
|
-
res.on('data', (chunk) => (data += chunk));
|
|
29
|
-
res.on('end', () => {
|
|
30
|
-
const status = res.statusCode || 0;
|
|
31
|
-
let parsed = null;
|
|
32
|
-
try {
|
|
33
|
-
parsed = data ? JSON.parse(data) : null;
|
|
34
|
-
} catch {
|
|
35
|
-
parsed = null;
|
|
36
|
-
}
|
|
37
|
-
if (status >= 200 && status < 300) return resolve(parsed);
|
|
38
|
-
const message = parsed?.message || `HTTP ${status}`;
|
|
39
|
-
const err = new Error(message);
|
|
40
|
-
err.status = status;
|
|
41
|
-
err.body = parsed;
|
|
42
|
-
return reject(err);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
);
|
|
46
|
-
req.on('error', reject);
|
|
47
|
-
if (payload) req.write(payload);
|
|
48
|
-
req.end();
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class UniWRTCClient {
|
|
6
|
+
class UniWRTCClient {
|
|
53
7
|
constructor(serverUrl, options = {}) {
|
|
54
8
|
this.serverUrl = serverUrl;
|
|
9
|
+
this.ws = null;
|
|
55
10
|
this.clientId = null;
|
|
56
11
|
this.sessionId = null;
|
|
57
|
-
this.
|
|
58
|
-
|
|
12
|
+
this.peers = new Map();
|
|
59
13
|
this.options = {
|
|
60
14
|
autoReconnect: true,
|
|
61
15
|
reconnectDelay: 3000,
|
|
62
|
-
pollIntervalMs: 500,
|
|
63
|
-
roomId: options.roomId || 'default',
|
|
64
16
|
...options
|
|
65
17
|
};
|
|
66
|
-
|
|
67
18
|
this.eventHandlers = {
|
|
68
19
|
'connected': [],
|
|
69
20
|
'disconnected': [],
|
|
@@ -78,147 +29,128 @@ export class UniWRTCClient {
|
|
|
78
29
|
};
|
|
79
30
|
}
|
|
80
31
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
apiUrl(pathname, extraSearch = {}) {
|
|
90
|
-
const origin = this.baseOrigin();
|
|
91
|
-
const url = new URL(origin + pathname);
|
|
92
|
-
url.searchParams.set('room', this.options.roomId);
|
|
93
|
-
for (const [k, v] of Object.entries(extraSearch)) {
|
|
94
|
-
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
95
|
-
}
|
|
96
|
-
return url.toString();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async postJson(pathname, body) {
|
|
100
|
-
return requestJson(this.apiUrl(pathname), { method: 'POST', body });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
startPolling() {
|
|
104
|
-
if (this._pollTimer) return;
|
|
105
|
-
this._pollTimer = setInterval(() => {
|
|
106
|
-
this.pollOnce().catch((err) => {
|
|
107
|
-
this.emit('error', { message: err?.message || String(err) });
|
|
108
|
-
});
|
|
109
|
-
}, this.options.pollIntervalMs);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
stopPolling() {
|
|
113
|
-
if (this._pollTimer) {
|
|
114
|
-
clearInterval(this._pollTimer);
|
|
115
|
-
this._pollTimer = null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async connect() {
|
|
120
|
-
try {
|
|
121
|
-
const welcome = await this.postJson('/api/connect', {});
|
|
122
|
-
this.clientId = welcome?.clientId;
|
|
123
|
-
if (!this.clientId) throw new Error('Missing clientId from server');
|
|
32
|
+
connect() {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
try {
|
|
35
|
+
// Get WebSocket class (browser or Node.js)
|
|
36
|
+
const WSClass = typeof WebSocket !== 'undefined' ? WebSocket : require('ws');
|
|
37
|
+
this.ws = new WSClass(this.serverUrl);
|
|
124
38
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
customId: this.options.customPeerId
|
|
129
|
-
});
|
|
130
|
-
this.clientId = res?.clientId || this.clientId;
|
|
131
|
-
}
|
|
39
|
+
this.ws.onopen = () => {
|
|
40
|
+
console.log('Connected to signaling server');
|
|
41
|
+
};
|
|
132
42
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
43
|
+
this.ws.onmessage = (event) => {
|
|
44
|
+
try {
|
|
45
|
+
const message = JSON.parse(event.data);
|
|
46
|
+
this.handleMessage(message);
|
|
47
|
+
|
|
48
|
+
if (message.type === 'welcome') {
|
|
49
|
+
this.clientId = message.clientId;
|
|
50
|
+
this.emit('connected', { clientId: this.clientId });
|
|
51
|
+
resolve(this.clientId);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Error parsing message:', error);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
this.ws.onclose = () => {
|
|
59
|
+
console.log('Disconnected from signaling server');
|
|
60
|
+
this.emit('disconnected');
|
|
61
|
+
|
|
62
|
+
if (this.options.autoReconnect) {
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
console.log('Attempting to reconnect...');
|
|
65
|
+
this.connect();
|
|
66
|
+
}, this.options.reconnectDelay);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
this.ws.onerror = (error) => {
|
|
71
|
+
console.error('WebSocket error:', error);
|
|
72
|
+
reject(error);
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
reject(error);
|
|
140
76
|
}
|
|
141
|
-
|
|
142
|
-
}
|
|
77
|
+
});
|
|
143
78
|
}
|
|
144
79
|
|
|
145
|
-
|
|
146
|
-
this.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
await this.postJson('/api/leave', { clientId: this.clientId });
|
|
151
|
-
} catch {
|
|
152
|
-
// ignore
|
|
153
|
-
}
|
|
80
|
+
disconnect() {
|
|
81
|
+
if (this.ws) {
|
|
82
|
+
this.options.autoReconnect = false;
|
|
83
|
+
this.ws.close();
|
|
84
|
+
this.ws = null;
|
|
154
85
|
}
|
|
155
|
-
this.emit('disconnected');
|
|
156
86
|
}
|
|
157
87
|
|
|
158
|
-
|
|
159
|
-
if (!this.clientId) throw new Error('Not connected');
|
|
160
|
-
if (this.sessionId === sessionId) return;
|
|
88
|
+
joinSession(sessionId) {
|
|
161
89
|
this.sessionId = sessionId;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
sessionId: joined?.sessionId,
|
|
166
|
-
clientId: joined?.clientId,
|
|
167
|
-
clients: joined?.clients
|
|
90
|
+
this.send({
|
|
91
|
+
type: 'join',
|
|
92
|
+
sessionId: sessionId
|
|
168
93
|
});
|
|
169
94
|
}
|
|
170
95
|
|
|
171
|
-
|
|
172
|
-
this.sessionId
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for (const msg of messages) this.handleMessage(msg);
|
|
96
|
+
leaveSession() {
|
|
97
|
+
if (this.sessionId) {
|
|
98
|
+
this.send({
|
|
99
|
+
type: 'leave',
|
|
100
|
+
sessionId: this.sessionId
|
|
101
|
+
});
|
|
102
|
+
this.sessionId = null;
|
|
103
|
+
}
|
|
180
104
|
}
|
|
181
105
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
106
|
+
sendOffer(offer, targetId = null) {
|
|
107
|
+
this.send({
|
|
108
|
+
type: 'offer',
|
|
109
|
+
offer: offer,
|
|
110
|
+
targetId: targetId,
|
|
111
|
+
sessionId: this.sessionId
|
|
188
112
|
});
|
|
189
113
|
}
|
|
190
114
|
|
|
191
|
-
sendOffer(offer, targetId) {
|
|
192
|
-
const offerSdp = typeof offer === 'string' ? offer : offer?.sdp;
|
|
193
|
-
return this.sendSignal({ type: 'offer', offer: offerSdp, targetId });
|
|
194
|
-
}
|
|
195
|
-
|
|
196
115
|
sendAnswer(answer, targetId) {
|
|
197
|
-
|
|
198
|
-
|
|
116
|
+
this.send({
|
|
117
|
+
type: 'answer',
|
|
118
|
+
answer: answer,
|
|
119
|
+
targetId: targetId,
|
|
120
|
+
sessionId: this.sessionId
|
|
121
|
+
});
|
|
199
122
|
}
|
|
200
123
|
|
|
201
|
-
sendIceCandidate(candidate, targetId) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return this.sendSignal({ type: 'ice-candidate', candidate: candidateText, targetId });
|
|
124
|
+
sendIceCandidate(candidate, targetId = null) {
|
|
125
|
+
this.send({
|
|
126
|
+
type: 'ice-candidate',
|
|
127
|
+
candidate: candidate,
|
|
128
|
+
targetId: targetId,
|
|
129
|
+
sessionId: this.sessionId
|
|
130
|
+
});
|
|
209
131
|
}
|
|
210
132
|
|
|
211
133
|
listRooms() {
|
|
212
|
-
|
|
134
|
+
this.send({
|
|
135
|
+
type: 'list-rooms'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
send(message) {
|
|
140
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
141
|
+
this.ws.send(JSON.stringify(message));
|
|
142
|
+
} else {
|
|
143
|
+
console.error('WebSocket is not connected');
|
|
144
|
+
}
|
|
213
145
|
}
|
|
214
146
|
|
|
215
147
|
handleMessage(message) {
|
|
216
148
|
switch (message.type) {
|
|
217
149
|
case 'welcome':
|
|
218
|
-
|
|
150
|
+
// Handled in connect(), but also surface donation message
|
|
151
|
+
console.log('[UniWRTC] If this helps, consider donating ❤️ → https://coff.ee/draederg');
|
|
219
152
|
break;
|
|
220
153
|
case 'joined':
|
|
221
|
-
this.sessionId = message.sessionId;
|
|
222
154
|
this.emit('joined', {
|
|
223
155
|
sessionId: message.sessionId,
|
|
224
156
|
clientId: message.clientId,
|
|
@@ -226,50 +158,76 @@ export class UniWRTCClient {
|
|
|
226
158
|
});
|
|
227
159
|
break;
|
|
228
160
|
case 'peer-joined':
|
|
229
|
-
this.emit('peer-joined', {
|
|
161
|
+
this.emit('peer-joined', {
|
|
162
|
+
sessionId: message.sessionId,
|
|
163
|
+
peerId: message.peerId
|
|
164
|
+
});
|
|
230
165
|
break;
|
|
231
166
|
case 'peer-left':
|
|
232
|
-
this.emit('peer-left', {
|
|
167
|
+
this.emit('peer-left', {
|
|
168
|
+
sessionId: message.sessionId,
|
|
169
|
+
peerId: message.peerId
|
|
170
|
+
});
|
|
233
171
|
break;
|
|
234
172
|
case 'offer':
|
|
235
|
-
this.emit('offer', {
|
|
173
|
+
this.emit('offer', {
|
|
174
|
+
peerId: message.peerId,
|
|
175
|
+
offer: message.offer
|
|
176
|
+
});
|
|
236
177
|
break;
|
|
237
178
|
case 'answer':
|
|
238
|
-
this.emit('answer', {
|
|
179
|
+
this.emit('answer', {
|
|
180
|
+
peerId: message.peerId,
|
|
181
|
+
answer: message.answer
|
|
182
|
+
});
|
|
239
183
|
break;
|
|
240
184
|
case 'ice-candidate':
|
|
241
|
-
this.emit('ice-candidate', {
|
|
185
|
+
this.emit('ice-candidate', {
|
|
186
|
+
peerId: message.peerId,
|
|
187
|
+
candidate: message.candidate
|
|
188
|
+
});
|
|
189
|
+
break;
|
|
190
|
+
case 'room-list':
|
|
191
|
+
this.emit('room-list', {
|
|
192
|
+
rooms: message.rooms
|
|
193
|
+
});
|
|
242
194
|
break;
|
|
243
195
|
case 'error':
|
|
244
|
-
this.emit('error', {
|
|
196
|
+
this.emit('error', {
|
|
197
|
+
message: message.message
|
|
198
|
+
});
|
|
245
199
|
break;
|
|
246
200
|
default:
|
|
247
|
-
|
|
248
|
-
break;
|
|
201
|
+
console.warn('Unknown message type:', message.type);
|
|
249
202
|
}
|
|
250
203
|
}
|
|
251
204
|
|
|
252
205
|
on(event, handler) {
|
|
253
|
-
if (this.eventHandlers[event])
|
|
206
|
+
if (this.eventHandlers[event]) {
|
|
207
|
+
this.eventHandlers[event].push(handler);
|
|
208
|
+
}
|
|
254
209
|
}
|
|
255
210
|
|
|
256
211
|
off(event, handler) {
|
|
257
212
|
if (this.eventHandlers[event]) {
|
|
258
|
-
this.eventHandlers[event] = this.eventHandlers[event].filter(
|
|
213
|
+
this.eventHandlers[event] = this.eventHandlers[event].filter(h => h !== handler);
|
|
259
214
|
}
|
|
260
215
|
}
|
|
261
216
|
|
|
262
217
|
emit(event, data) {
|
|
263
218
|
if (this.eventHandlers[event]) {
|
|
264
|
-
|
|
219
|
+
this.eventHandlers[event].forEach(handler => {
|
|
265
220
|
try {
|
|
266
221
|
handler(data);
|
|
267
222
|
} catch (error) {
|
|
268
223
|
console.error(`Error in ${event} handler:`, error);
|
|
269
224
|
}
|
|
270
|
-
}
|
|
225
|
+
});
|
|
271
226
|
}
|
|
272
227
|
}
|
|
273
228
|
}
|
|
274
229
|
|
|
275
|
-
|
|
230
|
+
// Export for Node.js and browser
|
|
231
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
232
|
+
module.exports = { UniWRTCClient };
|
|
233
|
+
}
|
package/package.json
CHANGED
package/src/client-cloudflare.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* UniWRTC Client -
|
|
3
|
-
*
|
|
2
|
+
* UniWRTC Client - Updated for Cloudflare deployment
|
|
3
|
+
* WebRTC Signaling Client Library
|
|
4
|
+
* Browser-only version
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
class UniWRTCClient {
|
|
7
8
|
constructor(serverUrl, options = {}) {
|
|
8
|
-
|
|
9
|
+
// Support both direct URLs and room parameter format
|
|
10
|
+
if (!serverUrl.includes('?')) {
|
|
11
|
+
const roomId = options.roomId || 'default';
|
|
12
|
+
this.serverUrl = serverUrl + (serverUrl.endsWith('/') ? '' : '/') + `signaling?room=${roomId}`;
|
|
13
|
+
} else {
|
|
14
|
+
this.serverUrl = serverUrl;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.ws = null;
|
|
9
18
|
this.clientId = null;
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
12
|
-
|
|
19
|
+
this.roomId = options.roomId || 'default';
|
|
20
|
+
this.peers = new Map();
|
|
21
|
+
this._connectedOnce = false;
|
|
13
22
|
this.options = {
|
|
14
23
|
autoReconnect: true,
|
|
15
24
|
reconnectDelay: 3000,
|
|
16
|
-
pollIntervalMs: 500,
|
|
17
|
-
roomId: options.roomId || 'default',
|
|
18
25
|
...options
|
|
19
26
|
};
|
|
20
|
-
|
|
21
27
|
this.eventHandlers = {
|
|
22
28
|
'connected': [],
|
|
23
29
|
'disconnected': [],
|
|
@@ -32,158 +38,122 @@ class UniWRTCClient {
|
|
|
32
38
|
};
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const url = new URL(origin + pathname);
|
|
46
|
-
url.searchParams.set('room', this.options.roomId);
|
|
47
|
-
for (const [k, v] of Object.entries(extraSearch)) {
|
|
48
|
-
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
49
|
-
}
|
|
50
|
-
return url.toString();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async postJson(pathname, body) {
|
|
54
|
-
const res = await fetch(this.apiUrl(pathname), {
|
|
55
|
-
method: 'POST',
|
|
56
|
-
headers: { 'Content-Type': 'application/json' },
|
|
57
|
-
body: JSON.stringify(body)
|
|
58
|
-
});
|
|
59
|
-
const data = await res.json().catch(() => null);
|
|
60
|
-
if (!res.ok) {
|
|
61
|
-
const message = data?.message || `HTTP ${res.status}`;
|
|
62
|
-
throw new Error(message);
|
|
63
|
-
}
|
|
64
|
-
return data;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
startPolling() {
|
|
68
|
-
if (this._pollTimer) return;
|
|
69
|
-
this._pollTimer = setInterval(() => {
|
|
70
|
-
this.pollOnce().catch((err) => {
|
|
71
|
-
this.emit('error', { message: err?.message || String(err) });
|
|
72
|
-
});
|
|
73
|
-
}, this.options.pollIntervalMs);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
stopPolling() {
|
|
77
|
-
if (this._pollTimer) {
|
|
78
|
-
clearInterval(this._pollTimer);
|
|
79
|
-
this._pollTimer = null;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async connect() {
|
|
84
|
-
try {
|
|
85
|
-
const welcome = await this.postJson('/api/connect', {});
|
|
86
|
-
this.clientId = welcome.clientId;
|
|
87
|
-
|
|
88
|
-
if (this.options.customPeerId) {
|
|
89
|
-
const res = await this.postJson('/api/set-id', {
|
|
90
|
-
clientId: this.clientId,
|
|
91
|
-
customId: this.options.customPeerId
|
|
92
|
-
});
|
|
93
|
-
this.clientId = res.clientId;
|
|
94
|
-
}
|
|
41
|
+
connect() {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
try {
|
|
44
|
+
// Convert https to wss, http to ws
|
|
45
|
+
let wsUrl = this.serverUrl;
|
|
46
|
+
if (wsUrl.startsWith('https://')) {
|
|
47
|
+
wsUrl = 'wss://' + wsUrl.substring(8);
|
|
48
|
+
} else if (wsUrl.startsWith('http://')) {
|
|
49
|
+
wsUrl = 'ws://' + wsUrl.substring(7);
|
|
50
|
+
}
|
|
95
51
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
52
|
+
this.ws = new WebSocket(wsUrl);
|
|
53
|
+
|
|
54
|
+
this.ws.onopen = () => {
|
|
55
|
+
console.log('Connected to signaling server');
|
|
56
|
+
|
|
57
|
+
// Send custom peer ID if provided
|
|
58
|
+
if (this.options.customPeerId) {
|
|
59
|
+
this.send({
|
|
60
|
+
type: 'set-id',
|
|
61
|
+
customId: this.options.customPeerId
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.ws.onmessage = (event) => {
|
|
67
|
+
try {
|
|
68
|
+
const message = JSON.parse(event.data);
|
|
69
|
+
this.handleMessage(message);
|
|
70
|
+
|
|
71
|
+
if (message.type === 'welcome' && !this._connectedOnce) {
|
|
72
|
+
this.clientId = message.clientId;
|
|
73
|
+
this._connectedOnce = true;
|
|
74
|
+
this.emit('connected', { clientId: this.clientId });
|
|
75
|
+
resolve(this.clientId);
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error parsing message:', error);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.ws.onclose = () => {
|
|
83
|
+
console.log('Disconnected from signaling server');
|
|
84
|
+
this.emit('disconnected');
|
|
85
|
+
|
|
86
|
+
if (this.options.autoReconnect) {
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
console.log('Attempting to reconnect...');
|
|
89
|
+
this.connect();
|
|
90
|
+
}, this.options.reconnectDelay);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.ws.onerror = (error) => {
|
|
95
|
+
console.error('WebSocket error:', error);
|
|
96
|
+
reject(error);
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
reject(error);
|
|
103
100
|
}
|
|
104
|
-
|
|
105
|
-
}
|
|
101
|
+
});
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
await this.postJson('/api/leave', { clientId: this.clientId });
|
|
114
|
-
} catch {
|
|
115
|
-
// ignore
|
|
116
|
-
}
|
|
104
|
+
disconnect() {
|
|
105
|
+
if (this.ws) {
|
|
106
|
+
this.options.autoReconnect = false;
|
|
107
|
+
this.ws.close();
|
|
108
|
+
this.ws = null;
|
|
117
109
|
}
|
|
118
|
-
this.emit('disconnected');
|
|
119
110
|
}
|
|
120
111
|
|
|
121
|
-
|
|
122
|
-
if (!this.clientId) throw new Error('Not connected');
|
|
123
|
-
if (this.sessionId === sessionId) return;
|
|
112
|
+
joinSession(sessionId) {
|
|
124
113
|
this.sessionId = sessionId;
|
|
125
|
-
|
|
126
|
-
const joined = await this.postJson('/api/join', {
|
|
127
|
-
clientId: this.clientId,
|
|
128
|
-
sessionId
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
this.emit('joined', {
|
|
132
|
-
sessionId: joined.sessionId,
|
|
133
|
-
clientId: joined.clientId,
|
|
134
|
-
clients: joined.clients
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async leaveSession() {
|
|
139
|
-
this.sessionId = null;
|
|
114
|
+
// Durable Objects handle session joining automatically via room parameter
|
|
140
115
|
}
|
|
141
116
|
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
const res = await fetch(url, { method: 'GET' });
|
|
146
|
-
const data = await res.json().catch(() => null);
|
|
147
|
-
if (!res.ok) {
|
|
148
|
-
throw new Error(data?.message || `HTTP ${res.status}`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const messages = Array.isArray(data?.messages) ? data.messages : [];
|
|
152
|
-
for (const msg of messages) {
|
|
153
|
-
this.handleMessage(msg);
|
|
117
|
+
leaveSession() {
|
|
118
|
+
if (this.sessionId) {
|
|
119
|
+
this.sessionId = null;
|
|
154
120
|
}
|
|
155
121
|
}
|
|
156
122
|
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
123
|
+
send(message) {
|
|
124
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
125
|
+
this.ws.send(JSON.stringify(message));
|
|
126
|
+
} else {
|
|
127
|
+
console.warn('WebSocket is not connected');
|
|
128
|
+
}
|
|
164
129
|
}
|
|
165
130
|
|
|
166
131
|
sendOffer(offer, targetId) {
|
|
167
|
-
|
|
168
|
-
|
|
132
|
+
this.send({
|
|
133
|
+
type: 'offer',
|
|
134
|
+
offer: offer,
|
|
135
|
+
targetId: targetId
|
|
136
|
+
});
|
|
169
137
|
}
|
|
170
138
|
|
|
171
139
|
sendAnswer(answer, targetId) {
|
|
172
|
-
|
|
173
|
-
|
|
140
|
+
this.send({
|
|
141
|
+
type: 'answer',
|
|
142
|
+
answer: answer,
|
|
143
|
+
targetId: targetId
|
|
144
|
+
});
|
|
174
145
|
}
|
|
175
146
|
|
|
176
147
|
sendIceCandidate(candidate, targetId) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
: candidate;
|
|
183
|
-
return this.sendSignal({ type: 'ice-candidate', candidate: candidateText, targetId });
|
|
148
|
+
this.send({
|
|
149
|
+
type: 'ice-candidate',
|
|
150
|
+
candidate: candidate,
|
|
151
|
+
targetId: targetId
|
|
152
|
+
});
|
|
184
153
|
}
|
|
185
154
|
|
|
186
155
|
listRooms() {
|
|
156
|
+
// Durable Objects don't expose room listing
|
|
187
157
|
console.log('Room listing not available with Durable Objects');
|
|
188
158
|
}
|
|
189
159
|
|
|
@@ -215,6 +185,7 @@ class UniWRTCClient {
|
|
|
215
185
|
switch (message.type) {
|
|
216
186
|
case 'welcome':
|
|
217
187
|
this.clientId = message.clientId;
|
|
188
|
+
console.log('[UniWRTC] If this helps, consider donating ❤️ → https://coff.ee/draederg');
|
|
218
189
|
break;
|
|
219
190
|
case 'joined':
|
|
220
191
|
this.sessionId = message.sessionId;
|
|
@@ -254,15 +225,25 @@ class UniWRTCClient {
|
|
|
254
225
|
candidate: message.candidate
|
|
255
226
|
});
|
|
256
227
|
break;
|
|
228
|
+
case 'room-list':
|
|
229
|
+
this.emit('room-list', {
|
|
230
|
+
rooms: message.rooms
|
|
231
|
+
});
|
|
232
|
+
break;
|
|
257
233
|
case 'error':
|
|
258
234
|
this.emit('error', {
|
|
259
235
|
message: message.message
|
|
260
236
|
});
|
|
261
237
|
break;
|
|
238
|
+
case 'chat':
|
|
239
|
+
this.emit('chat', {
|
|
240
|
+
text: message.text,
|
|
241
|
+
peerId: message.peerId,
|
|
242
|
+
sessionId: message.sessionId
|
|
243
|
+
});
|
|
244
|
+
break;
|
|
262
245
|
default:
|
|
263
246
|
console.log('Unknown message type:', message.type);
|
|
264
247
|
}
|
|
265
248
|
}
|
|
266
249
|
}
|
|
267
|
-
|
|
268
|
-
export default UniWRTCClient;
|
package/src/main.js
CHANGED
|
@@ -21,7 +21,7 @@ document.getElementById('app').innerHTML = `
|
|
|
21
21
|
<div class="connection-controls">
|
|
22
22
|
<div>
|
|
23
23
|
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Server URL</label>
|
|
24
|
-
<input type="text" id="serverUrl" data-testid="serverUrl" placeholder="
|
|
24
|
+
<input type="text" id="serverUrl" data-testid="serverUrl" placeholder="wss://signal.peer.ooo or ws://localhost:8080" value="wss://signal.peer.ooo">
|
|
25
25
|
</div>
|
|
26
26
|
<div>
|
|
27
27
|
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Room / Session ID</label>
|
|
@@ -146,43 +146,6 @@ function updatePeerList() {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function normalizeSessionDescription(descOrSdp, fallbackType) {
|
|
150
|
-
if (!descOrSdp) {
|
|
151
|
-
throw new Error(`Missing ${fallbackType} SDP`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (typeof descOrSdp === 'string') {
|
|
155
|
-
return { type: fallbackType, sdp: descOrSdp };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (typeof descOrSdp === 'object') {
|
|
159
|
-
if (typeof descOrSdp.sdp === 'string') {
|
|
160
|
-
return {
|
|
161
|
-
type: descOrSdp.type || fallbackType,
|
|
162
|
-
sdp: descOrSdp.sdp
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return descOrSdp;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function normalizeIceCandidate(candidateOrText) {
|
|
171
|
-
if (!candidateOrText) return null;
|
|
172
|
-
if (typeof candidateOrText === 'string') {
|
|
173
|
-
const [candidate, sdpMidRaw, sdpMLineIndexRaw] = candidateOrText.split('|');
|
|
174
|
-
if (!candidate) return null;
|
|
175
|
-
const ice = { candidate };
|
|
176
|
-
if (sdpMidRaw) ice.sdpMid = sdpMidRaw;
|
|
177
|
-
if (sdpMLineIndexRaw !== undefined && sdpMLineIndexRaw !== '') {
|
|
178
|
-
const idx = Number(sdpMLineIndexRaw);
|
|
179
|
-
if (!Number.isNaN(idx)) ice.sdpMLineIndex = idx;
|
|
180
|
-
}
|
|
181
|
-
return ice;
|
|
182
|
-
}
|
|
183
|
-
return candidateOrText;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
149
|
window.connect = async function() {
|
|
187
150
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
|
188
151
|
const roomId = document.getElementById('roomId').value.trim();
|
|
@@ -199,15 +162,15 @@ window.connect = async function() {
|
|
|
199
162
|
|
|
200
163
|
try {
|
|
201
164
|
log(`Connecting to ${serverUrl}...`, 'info');
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
165
|
+
|
|
166
|
+
// For Cloudflare, use /ws endpoint with room ID query param
|
|
167
|
+
let finalUrl = serverUrl;
|
|
168
|
+
if (serverUrl.includes('signal.peer.ooo')) {
|
|
169
|
+
finalUrl = `${serverUrl.replace(/\/$/, '')}/ws?room=${roomId}`;
|
|
170
|
+
log(`Using Cloudflare Durable Objects with session: ${roomId}`, 'info');
|
|
206
171
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
client = new UniWRTCClient(serverUrl, { autoReconnect: false, roomId });
|
|
210
|
-
log(`Using HTTP polling (no WebSockets) for room: ${roomId}`, 'info');
|
|
172
|
+
|
|
173
|
+
client = new UniWRTCClient(finalUrl, { autoReconnect: false });
|
|
211
174
|
|
|
212
175
|
client.on('connected', (data) => {
|
|
213
176
|
log(`Connected with client ID: ${data.clientId}`, 'success');
|
|
@@ -261,7 +224,7 @@ window.connect = async function() {
|
|
|
261
224
|
client.on('offer', async (data) => {
|
|
262
225
|
log(`Received offer from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
263
226
|
const pc = peerConnections.get(data.peerId) || await createPeerConnection(data.peerId, false);
|
|
264
|
-
await pc.setRemoteDescription(
|
|
227
|
+
await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
|
|
265
228
|
const answer = await pc.createAnswer();
|
|
266
229
|
await pc.setLocalDescription(answer);
|
|
267
230
|
client.sendAnswer(answer, data.peerId);
|
|
@@ -272,7 +235,7 @@ window.connect = async function() {
|
|
|
272
235
|
log(`Received answer from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
273
236
|
const pc = peerConnections.get(data.peerId);
|
|
274
237
|
if (pc) {
|
|
275
|
-
await pc.setRemoteDescription(
|
|
238
|
+
await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
|
|
276
239
|
}
|
|
277
240
|
});
|
|
278
241
|
|
|
@@ -280,10 +243,7 @@ window.connect = async function() {
|
|
|
280
243
|
log(`Received ICE candidate from ${data.peerId.substring(0, 6)}...`, 'info');
|
|
281
244
|
const pc = peerConnections.get(data.peerId);
|
|
282
245
|
if (pc && data.candidate) {
|
|
283
|
-
|
|
284
|
-
if (iceInit) {
|
|
285
|
-
await pc.addIceCandidate(new RTCIceCandidate(iceInit));
|
|
286
|
-
}
|
|
246
|
+
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
|
|
287
247
|
}
|
|
288
248
|
});
|
|
289
249
|
|
|
@@ -329,9 +289,9 @@ async function createPeerConnection(peerId, shouldInitiate) {
|
|
|
329
289
|
log(`Creating peer connection with ${peerId.substring(0, 6)}... (shouldInitiate: ${shouldInitiate})`, 'info');
|
|
330
290
|
|
|
331
291
|
const pc = new RTCPeerConnection({
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
292
|
+
iceServers: [
|
|
293
|
+
{ urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] }
|
|
294
|
+
]
|
|
335
295
|
});
|
|
336
296
|
|
|
337
297
|
pc.onicecandidate = (event) => {
|
package/simple-peer-adapter.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* simple-peer ↔ UniWRTC signaling adapter (SDP text only)
|
|
3
|
-
*
|
|
4
|
-
* UniWRTC wire format after this repo's change:
|
|
5
|
-
* - offer: { type: 'offer', offer: '<sdp string>', targetId, sessionId? }
|
|
6
|
-
* - answer:{ type: 'answer', answer:'<sdp string>', targetId, sessionId? }
|
|
7
|
-
* - ice: { type: 'ice-candidate', candidate: RTCIceCandidateInit, targetId, sessionId? }
|
|
8
|
-
*
|
|
9
|
-
* This adapter maps simple-peer "signal" objects to/from that wire format.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
function normalizeSdp(value) {
|
|
13
|
-
if (!value) return null;
|
|
14
|
-
if (typeof value === 'string') return value;
|
|
15
|
-
if (typeof value === 'object' && typeof value.sdp === 'string') return value.sdp;
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isIceCandidateSignal(signal) {
|
|
20
|
-
return !!signal && typeof signal === 'object' && typeof signal.candidate === 'string';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function parseIceCandidateText(candidateText) {
|
|
24
|
-
if (!candidateText || typeof candidateText !== 'string') return null;
|
|
25
|
-
const [candidate, sdpMidRaw, sdpMLineIndexRaw] = candidateText.split('|');
|
|
26
|
-
if (!candidate) return null;
|
|
27
|
-
const ice = { candidate };
|
|
28
|
-
if (sdpMidRaw) ice.sdpMid = sdpMidRaw;
|
|
29
|
-
if (sdpMLineIndexRaw !== undefined && sdpMLineIndexRaw !== '') {
|
|
30
|
-
const idx = Number(sdpMLineIndexRaw);
|
|
31
|
-
if (!Number.isNaN(idx)) ice.sdpMLineIndex = idx;
|
|
32
|
-
}
|
|
33
|
-
return ice;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Deterministically chooses a single initiator for a given peer pair.
|
|
38
|
-
* Returns true for exactly one side when both sides use the same two IDs.
|
|
39
|
-
*/
|
|
40
|
-
export function chooseDeterministicInitiator(localPeerId, remotePeerId) {
|
|
41
|
-
if (!localPeerId || !remotePeerId) {
|
|
42
|
-
throw new Error('localPeerId and remotePeerId are required');
|
|
43
|
-
}
|
|
44
|
-
return String(localPeerId) < String(remotePeerId);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Sends a simple-peer signal via UniWRTCClient.
|
|
49
|
-
*
|
|
50
|
-
* @param {object} client UniWRTCClient instance
|
|
51
|
-
* @param {object} signal simple-peer signal object
|
|
52
|
-
* @param {string} targetId peer id to target
|
|
53
|
-
*/
|
|
54
|
-
export function sendSimplePeerSignal(client, signal, targetId) {
|
|
55
|
-
if (!client) throw new Error('client is required');
|
|
56
|
-
if (!signal) throw new Error('signal is required');
|
|
57
|
-
if (!targetId) throw new Error('targetId is required');
|
|
58
|
-
|
|
59
|
-
if (signal.type === 'offer') {
|
|
60
|
-
const sdp = normalizeSdp(signal.sdp ?? signal);
|
|
61
|
-
if (!sdp) throw new Error('offer SDP missing');
|
|
62
|
-
client.sendOffer(sdp, targetId);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (signal.type === 'answer') {
|
|
67
|
-
const sdp = normalizeSdp(signal.sdp ?? signal);
|
|
68
|
-
if (!sdp) throw new Error('answer SDP missing');
|
|
69
|
-
client.sendAnswer(sdp, targetId);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// simple-peer ICE candidate signal has shape: { candidate, sdpMid, sdpMLineIndex }
|
|
74
|
-
if (isIceCandidateSignal(signal) || signal.type === 'ice-candidate' || signal.type === 'candidate') {
|
|
75
|
-
client.sendIceCandidate(signal, targetId);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// simple-peer can emit renegotiation / transceiver requests.
|
|
80
|
-
// UniWRTC server in this repo doesn't route arbitrary signal types.
|
|
81
|
-
throw new Error(`Unsupported simple-peer signal type for UniWRTC transport: ${signal.type || '(unknown)'}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Attaches UniWRTCClient events to a simple-peer instance.
|
|
86
|
-
*
|
|
87
|
-
* @param {object} client UniWRTCClient instance
|
|
88
|
-
* @param {object} peer simple-peer instance (must have peer.signal())
|
|
89
|
-
* @returns {() => void} cleanup function
|
|
90
|
-
*/
|
|
91
|
-
export function attachUniWRTCToSimplePeer(client, peer) {
|
|
92
|
-
if (!client) throw new Error('client is required');
|
|
93
|
-
if (!peer || typeof peer.signal !== 'function') throw new Error('peer.signal(...) is required');
|
|
94
|
-
|
|
95
|
-
const onOffer = (data) => {
|
|
96
|
-
const sdp = normalizeSdp(data.offer);
|
|
97
|
-
if (sdp) peer.signal({ type: 'offer', sdp });
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const onAnswer = (data) => {
|
|
101
|
-
const sdp = normalizeSdp(data.answer);
|
|
102
|
-
if (sdp) peer.signal({ type: 'answer', sdp });
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const onIce = (data) => {
|
|
106
|
-
if (!data?.candidate) return;
|
|
107
|
-
if (typeof data.candidate === 'string') {
|
|
108
|
-
const ice = parseIceCandidateText(data.candidate);
|
|
109
|
-
if (ice) peer.signal(ice);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
peer.signal(data.candidate);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
client.on('offer', onOffer);
|
|
116
|
-
client.on('answer', onAnswer);
|
|
117
|
-
client.on('ice-candidate', onIce);
|
|
118
|
-
|
|
119
|
-
return () => {
|
|
120
|
-
client.off('offer', onOffer);
|
|
121
|
-
client.off('answer', onAnswer);
|
|
122
|
-
client.off('ice-candidate', onIce);
|
|
123
|
-
};
|
|
124
|
-
}
|