uniwrtc 1.0.8 → 1.1.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.
@@ -0,0 +1,6 @@
1
+ # Nostr notes
2
+
3
+ The demo page now defaults to Nostr (no UI toggle).
4
+
5
+ - Nostr client: [src/nostr/nostrClient.js](src/nostr/nostrClient.js)
6
+ - UI integration: [src/main.js](src/main.js)
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.1.0",
4
4
  "description": "A universal WebRTC signaling service",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -20,7 +20,9 @@
20
20
  ],
21
21
  "author": "",
22
22
  "license": "MIT",
23
- "dependencies": {},
23
+ "dependencies": {
24
+ "nostr-tools": "^2.9.0"
25
+ },
24
26
  "devDependencies": {
25
27
  "@playwright/test": "^1.57.0",
26
28
  "vite": "^6.0.6"