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 CHANGED
@@ -1,13 +1,265 @@
1
1
  /**
2
2
  * UniWRTC Client - WebRTC Signaling Client Library
3
- * Browser version (HTTP polling; no WebSockets)
3
+ * Browser-only version
4
4
  */
5
5
 
6
- import UniWRTCClient from './src/client-cloudflare.js';
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 - Node.js client (HTTP polling; no WebSockets)
2
+ * UniWRTC Client - WebRTC Signaling Client Library
3
+ * Simplifies connection to the UniWRTC signaling server
3
4
  */
4
5
 
5
- import http from 'http';
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._pollTimer = null;
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
- baseOrigin() {
82
- const parsed = new URL(this.serverUrl);
83
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
84
- throw new Error('Server URL must start with http(s):// (WebSockets are disabled)');
85
- }
86
- return parsed.origin;
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
- if (this.options.customPeerId) {
126
- const res = await this.postJson('/api/set-id', {
127
- clientId: this.clientId,
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
- this.emit('connected', { clientId: this.clientId });
134
- this.startPolling();
135
- return this.clientId;
136
- } catch (error) {
137
- this.emit('error', { message: error?.message || String(error) });
138
- if (this.options.autoReconnect) {
139
- setTimeout(() => this.connect(), this.options.reconnectDelay);
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
- throw error;
142
- }
77
+ });
143
78
  }
144
79
 
145
- async disconnect() {
146
- this.options.autoReconnect = false;
147
- this.stopPolling();
148
- if (this.clientId) {
149
- try {
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
- async joinSession(sessionId) {
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
- const joined = await this.postJson('/api/join', { clientId: this.clientId, sessionId });
164
- this.emit('joined', {
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
- async leaveSession() {
172
- this.sessionId = null;
173
- }
174
-
175
- async pollOnce() {
176
- if (!this.clientId) return;
177
- const data = await requestJson(this.apiUrl('/api/poll', { clientId: this.clientId }), { method: 'GET' });
178
- const messages = Array.isArray(data?.messages) ? data.messages : [];
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
- async sendSignal(payload) {
183
- if (!this.clientId) throw new Error('Not connected');
184
- await this.postJson('/api/signal', {
185
- clientId: this.clientId,
186
- sessionId: this.sessionId,
187
- ...payload
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
- const answerSdp = typeof answer === 'string' ? answer : answer?.sdp;
198
- return this.sendSignal({ type: 'answer', answer: answerSdp, targetId });
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
- const candidateText =
203
- typeof candidate === 'string'
204
- ? candidate
205
- : candidate && typeof candidate === 'object' && typeof candidate.candidate === 'string'
206
- ? `${candidate.candidate}|${candidate.sdpMid ?? ''}|${candidate.sdpMLineIndex ?? ''}`
207
- : candidate;
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
- console.log('Room listing not available with HTTP polling');
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
- this.clientId = message.clientId;
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', { sessionId: message.sessionId, peerId: message.peerId });
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', { sessionId: message.sessionId, peerId: message.peerId });
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', { peerId: message.peerId, offer: message.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', { peerId: message.peerId, answer: message.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', { peerId: message.peerId, candidate: message.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', { message: message.message });
196
+ this.emit('error', {
197
+ message: message.message
198
+ });
245
199
  break;
246
200
  default:
247
- // ignore
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]) this.eventHandlers[event].push(handler);
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((h) => h !== handler);
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
- for (const handler of this.eventHandlers[event]) {
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
- export default UniWRTCClient;
230
+ // Export for Node.js and browser
231
+ if (typeof module !== 'undefined' && module.exports) {
232
+ module.exports = { UniWRTCClient };
233
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniwrtc",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "A universal WebRTC signaling service",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -1,23 +1,29 @@
1
1
  /**
2
- * UniWRTC Client - Cloudflare Durable Objects (HTTP polling)
3
- * No WebSockets.
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
- this.serverUrl = serverUrl;
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.sessionId = null;
11
- this._pollTimer = null;
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
- baseOrigin() {
36
- const parsed = new URL(this.serverUrl);
37
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
38
- throw new Error('Server URL must start with http(s):// (WebSockets are disabled)');
39
- }
40
- return parsed.origin;
41
- }
42
-
43
- apiUrl(pathname, extraSearch = {}) {
44
- const origin = this.baseOrigin();
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
- this.emit('connected', { clientId: this.clientId });
97
- this.startPolling();
98
- return this.clientId;
99
- } catch (error) {
100
- this.emit('error', { message: error?.message || String(error) });
101
- if (this.options.autoReconnect) {
102
- setTimeout(() => this.connect(), this.options.reconnectDelay);
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
- throw error;
105
- }
101
+ });
106
102
  }
107
103
 
108
- async disconnect() {
109
- this.options.autoReconnect = false;
110
- this.stopPolling();
111
- if (this.clientId) {
112
- try {
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
- async joinSession(sessionId) {
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
- async pollOnce() {
143
- if (!this.clientId) return;
144
- const url = this.apiUrl('/api/poll', { clientId: this.clientId });
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
- async sendSignal(payload) {
158
- if (!this.clientId) throw new Error('Not connected');
159
- await this.postJson('/api/signal', {
160
- clientId: this.clientId,
161
- sessionId: this.sessionId,
162
- ...payload
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
- const offerSdp = typeof offer === 'string' ? offer : offer?.sdp;
168
- return this.sendSignal({ type: 'offer', offer: offerSdp, targetId });
132
+ this.send({
133
+ type: 'offer',
134
+ offer: offer,
135
+ targetId: targetId
136
+ });
169
137
  }
170
138
 
171
139
  sendAnswer(answer, targetId) {
172
- const answerSdp = typeof answer === 'string' ? answer : answer?.sdp;
173
- return this.sendSignal({ type: 'answer', answer: answerSdp, targetId });
140
+ this.send({
141
+ type: 'answer',
142
+ answer: answer,
143
+ targetId: targetId
144
+ });
174
145
  }
175
146
 
176
147
  sendIceCandidate(candidate, targetId) {
177
- const candidateText =
178
- typeof candidate === 'string'
179
- ? candidate
180
- : candidate && typeof candidate === 'object' && typeof candidate.candidate === 'string'
181
- ? `${candidate.candidate}|${candidate.sdpMid ?? ''}|${candidate.sdpMLineIndex ?? ''}`
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="https://signal.peer.ooo or http://localhost:8080" value="https://signal.peer.ooo">
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
- const isHttpUrl = /^https?:\/\//i.test(serverUrl);
204
- if (!isHttpUrl) {
205
- throw new Error('Server URL must start with http(s):// (WebSockets are disabled)');
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
- // HTTP polling signaling (no WebSockets)
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(normalizeSessionDescription(data.offer, 'offer'));
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(normalizeSessionDescription(data.answer, 'answer'));
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
- const iceInit = normalizeIceCandidate(data.candidate);
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
- // Avoid external STUN for reliability in restricted environments.
333
- // Host candidates are sufficient for same-device/browser-tab testing.
334
- iceServers: []
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) => {
@@ -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
- }