uniwrtc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * UniWRTC Client - Updated for Cloudflare deployment
3
+ * WebRTC Signaling Client Library
4
+ * Browser-only version
5
+ */
6
+
7
+ class UniWRTCClient {
8
+ constructor(serverUrl, options = {}) {
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;
18
+ this.clientId = null;
19
+ this.roomId = options.roomId || 'default';
20
+ this.peers = new Map();
21
+ this._connectedOnce = false;
22
+ this.options = {
23
+ autoReconnect: true,
24
+ reconnectDelay: 3000,
25
+ ...options
26
+ };
27
+ this.eventHandlers = {
28
+ 'connected': [],
29
+ 'disconnected': [],
30
+ 'joined': [],
31
+ 'peer-joined': [],
32
+ 'peer-left': [],
33
+ 'offer': [],
34
+ 'answer': [],
35
+ 'ice-candidate': [],
36
+ 'room-list': [],
37
+ 'error': []
38
+ };
39
+ }
40
+
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
+ }
51
+
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);
100
+ }
101
+ });
102
+ }
103
+
104
+ disconnect() {
105
+ if (this.ws) {
106
+ this.options.autoReconnect = false;
107
+ this.ws.close();
108
+ this.ws = null;
109
+ }
110
+ }
111
+
112
+ joinRoom(roomId) {
113
+ this.roomId = roomId;
114
+ // Durable Objects handle room joining automatically via room parameter
115
+ }
116
+
117
+ leaveRoom() {
118
+ if (this.roomId) {
119
+ this.roomId = null;
120
+ }
121
+ }
122
+
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
+ }
129
+ }
130
+
131
+ sendOffer(targetId, offer) {
132
+ this.send({
133
+ type: 'offer',
134
+ offer: offer,
135
+ targetId: targetId
136
+ });
137
+ }
138
+
139
+ sendAnswer(targetId, answer) {
140
+ this.send({
141
+ type: 'answer',
142
+ answer: answer,
143
+ targetId: targetId
144
+ });
145
+ }
146
+
147
+ sendIceCandidate(targetId, candidate) {
148
+ this.send({
149
+ type: 'ice-candidate',
150
+ candidate: candidate,
151
+ targetId: targetId
152
+ });
153
+ }
154
+
155
+ listRooms() {
156
+ // Durable Objects don't expose room listing
157
+ console.log('Room listing not available with Durable Objects');
158
+ }
159
+
160
+ on(event, handler) {
161
+ if (this.eventHandlers[event]) {
162
+ this.eventHandlers[event].push(handler);
163
+ }
164
+ }
165
+
166
+ off(event, handler) {
167
+ if (this.eventHandlers[event]) {
168
+ this.eventHandlers[event] = this.eventHandlers[event].filter(h => h !== handler);
169
+ }
170
+ }
171
+
172
+ emit(event, data) {
173
+ if (this.eventHandlers[event]) {
174
+ this.eventHandlers[event].forEach(handler => {
175
+ try {
176
+ handler(data);
177
+ } catch (error) {
178
+ console.error(`Error in ${event} handler:`, error);
179
+ }
180
+ });
181
+ }
182
+ }
183
+
184
+ handleMessage(message) {
185
+ switch (message.type) {
186
+ case 'welcome':
187
+ this.clientId = message.clientId;
188
+ console.log('[UniWRTC] If this helps, consider donating ā¤ļø → https://coff.ee/draederg');
189
+ break;
190
+ case 'joined':
191
+ this.roomId = message.roomId;
192
+ this.emit('joined', {
193
+ roomId: message.roomId,
194
+ clientId: message.clientId,
195
+ clients: message.clients
196
+ });
197
+ break;
198
+ case 'peer-joined':
199
+ this.emit('peer-joined', {
200
+ clientId: message.clientId
201
+ });
202
+ break;
203
+ case 'peer-left':
204
+ this.emit('peer-left', {
205
+ clientId: message.clientId
206
+ });
207
+ break;
208
+ case 'offer':
209
+ this.emit('offer', {
210
+ peerId: message.peerId,
211
+ offer: message.offer
212
+ });
213
+ break;
214
+ case 'answer':
215
+ this.emit('answer', {
216
+ peerId: message.peerId,
217
+ answer: message.answer
218
+ });
219
+ break;
220
+ case 'ice-candidate':
221
+ this.emit('ice-candidate', {
222
+ peerId: message.peerId,
223
+ candidate: message.candidate
224
+ });
225
+ break;
226
+ case 'room-list':
227
+ this.emit('room-list', {
228
+ rooms: message.rooms
229
+ });
230
+ break;
231
+ case 'error':
232
+ this.emit('error', {
233
+ message: message.message
234
+ });
235
+ break;
236
+ case 'chat':
237
+ this.emit('chat', {
238
+ text: message.text,
239
+ senderId: message.senderId,
240
+ roomId: message.roomId
241
+ });
242
+ break;
243
+ default:
244
+ console.log('Unknown message type:', message.type);
245
+ }
246
+ }
247
+ }
package/src/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * UniWRTC Cloudflare Worker
3
+ * WebRTC Signaling Service using Durable Objects
4
+ */
5
+
6
+ import { Room } from './room.js';
7
+
8
+ export { Room };
9
+
10
+ export default {
11
+ async fetch(request, env) {
12
+ const url = new URL(request.url);
13
+
14
+ // Health check
15
+ if (url.pathname === '/health') {
16
+ return new Response(JSON.stringify({ status: 'ok' }), {
17
+ headers: { 'Content-Type': 'application/json' }
18
+ });
19
+ }
20
+
21
+ // WebSocket signaling
22
+ if (url.pathname === '/signaling' || url.pathname === '/') {
23
+ const roomId = url.searchParams.get('room') || 'default';
24
+
25
+ // Get Durable Object stub for this room
26
+ const id = env.ROOMS.idFromName(roomId);
27
+ const roomStub = env.ROOMS.get(id);
28
+
29
+ // Forward request to Durable Object
30
+ return roomStub.fetch(request);
31
+ }
32
+
33
+ return new Response('Not Found', { status: 404 });
34
+ }
35
+ };
package/src/room.js ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Durable Object for WebRTC Signaling Room
3
+ * Manages peers in a room and routes signaling messages
4
+ */
5
+ export class Room {
6
+ constructor(state, env) {
7
+ this.state = state;
8
+ this.env = env;
9
+ this.clients = new Map(); // Map of clientId -> WebSocket
10
+ }
11
+
12
+ async fetch(request) {
13
+ if (request.headers.get('Upgrade') === 'websocket') {
14
+ const [client, server] = Object.values(new WebSocketPair());
15
+ server.accept();
16
+
17
+ const clientId = crypto.randomUUID().substring(0, 9);
18
+ this.clients.set(clientId, server);
19
+
20
+ console.log(`[Room] Client ${clientId} joined (total: ${this.clients.size})`);
21
+
22
+ // Send welcome message
23
+ server.send(JSON.stringify({
24
+ type: 'welcome',
25
+ clientId: clientId,
26
+ message: 'Connected to UniWRTC signaling room'
27
+ }));
28
+
29
+ // Notify existing clients
30
+ this.broadcast({
31
+ type: 'peer-joined',
32
+ clientId: clientId
33
+ }, clientId);
34
+
35
+ server.onmessage = async (event) => {
36
+ try {
37
+ const message = JSON.parse(event.data);
38
+ await this.handleMessage(clientId, message);
39
+ } catch (error) {
40
+ console.error('[Room] Message error:', error);
41
+ server.send(JSON.stringify({ type: 'error', message: error.message }));
42
+ }
43
+ };
44
+
45
+ server.onclose = () => {
46
+ console.log(`[Room] Client ${clientId} left`);
47
+ this.clients.delete(clientId);
48
+ this.broadcast({
49
+ type: 'peer-left',
50
+ peerId: clientId,
51
+ clientId: clientId
52
+ });
53
+ };
54
+
55
+ server.onerror = (error) => {
56
+ console.error('[Room] WebSocket error:', error);
57
+ };
58
+
59
+ return new Response(null, { status: 101, webSocket: client });
60
+ }
61
+
62
+ return new Response('Not a WebSocket request', { status: 400 });
63
+ }
64
+
65
+ async handleMessage(clientId, message) {
66
+ switch (message.type) {
67
+ case 'join':
68
+ await this.handleJoin(clientId, message);
69
+ break;
70
+ case 'set-id':
71
+ await this.handleSetId(clientId, message);
72
+ break;
73
+ case 'offer':
74
+ case 'answer':
75
+ case 'ice-candidate':
76
+ await this.handleSignaling(clientId, message);
77
+ break;
78
+ default:
79
+ console.log(`[Room] Unknown message type: ${message.type}`);
80
+ }
81
+ }
82
+
83
+ async handleJoin(clientId, message) {
84
+ const { roomId, peerId } = message;
85
+
86
+ // Get list of other peers
87
+ const peers = Array.from(this.clients.keys())
88
+ .filter(id => id !== clientId);
89
+
90
+ const client = this.clients.get(clientId);
91
+ if (client && client.readyState === WebSocket.OPEN) {
92
+ // Send joined confirmation
93
+ client.send(JSON.stringify({
94
+ type: 'joined',
95
+ roomId: roomId,
96
+ peerId: peerId || clientId,
97
+ clients: peers
98
+ }));
99
+ }
100
+
101
+ // Notify other peers
102
+ this.broadcast({
103
+ type: 'peer-joined',
104
+ peerId: peerId || clientId,
105
+ clientId: clientId
106
+ }, clientId);
107
+ }
108
+
109
+ async handleSetId(clientId, message) {
110
+ const { customId } = message;
111
+
112
+ if (!customId || customId.length < 3 || customId.length > 20) {
113
+ const client = this.clients.get(clientId);
114
+ if (client && client.readyState === WebSocket.OPEN) {
115
+ client.send(JSON.stringify({
116
+ type: 'error',
117
+ message: 'Custom ID must be 3-20 characters'
118
+ }));
119
+ }
120
+ return;
121
+ }
122
+
123
+ // Check if ID is already taken
124
+ let idTaken = false;
125
+ for (const [id, ws] of this.clients) {
126
+ if (id !== clientId && id === customId) {
127
+ idTaken = true;
128
+ break;
129
+ }
130
+ }
131
+
132
+ if (idTaken) {
133
+ const client = this.clients.get(clientId);
134
+ if (client && client.readyState === WebSocket.OPEN) {
135
+ client.send(JSON.stringify({
136
+ type: 'error',
137
+ message: 'Peer ID already taken'
138
+ }));
139
+ }
140
+ return;
141
+ }
142
+
143
+ // Update client ID
144
+ const ws = this.clients.get(clientId);
145
+ this.clients.delete(clientId);
146
+ this.clients.set(customId, ws);
147
+
148
+ console.log(`[Room] Client changed ID from ${clientId} to ${customId}`);
149
+
150
+ // Send confirmation
151
+ if (ws && ws.readyState === WebSocket.OPEN) {
152
+ ws.send(JSON.stringify({
153
+ type: 'welcome',
154
+ clientId: customId,
155
+ message: 'Custom peer ID set'
156
+ }));
157
+ }
158
+
159
+ // Notify others of ID change
160
+ this.broadcast({
161
+ type: 'peer-id-changed',
162
+ oldId: clientId,
163
+ newId: customId
164
+ });
165
+ }
166
+
167
+ async handleSignaling(clientId, message) {
168
+ const { targetId, type, offer, answer, candidate } = message;
169
+
170
+ if (!targetId) {
171
+ console.log(`[Room] Signaling without target`);
172
+ return;
173
+ }
174
+
175
+ const targetClient = this.clients.get(targetId);
176
+ if (!targetClient || targetClient.readyState !== WebSocket.OPEN) {
177
+ console.log(`[Room] Target client ${targetId} not found or closed`);
178
+ const client = this.clients.get(clientId);
179
+ if (client && client.readyState === WebSocket.OPEN) {
180
+ client.send(JSON.stringify({
181
+ type: 'error',
182
+ message: `Target peer ${targetId} not found`
183
+ }));
184
+ }
185
+ return;
186
+ }
187
+
188
+ console.log(`[Room] Routing ${type} from ${clientId} to ${targetId}`);
189
+
190
+ // Route signaling message to target
191
+ const forwardMessage = {
192
+ type: type,
193
+ peerId: clientId
194
+ };
195
+
196
+ if (offer) forwardMessage.offer = offer;
197
+ if (answer) forwardMessage.answer = answer;
198
+ if (candidate) forwardMessage.candidate = candidate;
199
+
200
+ targetClient.send(JSON.stringify(forwardMessage));
201
+ }
202
+
203
+ broadcast(message, excludeClientId = null) {
204
+ const payload = JSON.stringify(message);
205
+ for (const [clientId, client] of this.clients) {
206
+ if (clientId !== excludeClientId && client.readyState === WebSocket.OPEN) {
207
+ client.send(payload);
208
+ }
209
+ }
210
+ }
211
+ }
package/test.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Simple test script for UniWRTC
5
+ * Runs two clients and tests peer connections
6
+ */
7
+
8
+ const { UniWRTCClient } = require('./client.js');
9
+
10
+ async function sleep(ms) {
11
+ return new Promise(resolve => setTimeout(resolve, ms));
12
+ }
13
+
14
+ async function runTest() {
15
+ console.log('šŸš€ Starting UniWRTC Test...\n');
16
+
17
+ // Create two clients
18
+ const client1 = new UniWRTCClient('ws://localhost:8080');
19
+ const client2 = new UniWRTCClient('ws://localhost:8080');
20
+
21
+ try {
22
+ // Connect first client
23
+ console.log('šŸ“± Connecting Client 1...');
24
+ const id1 = await client1.connect();
25
+ console.log(`āœ… Client 1 connected with ID: ${id1}\n`);
26
+
27
+ await sleep(500);
28
+
29
+ // Connect second client
30
+ console.log('šŸ“± Connecting Client 2...');
31
+ const id2 = await client2.connect();
32
+ console.log(`āœ… Client 2 connected with ID: ${id2}\n`);
33
+
34
+ await sleep(500);
35
+
36
+ // Join same room
37
+ console.log('šŸ  Client 1 joining room: test-room');
38
+ client1.joinRoom('test-room');
39
+
40
+ await sleep(500);
41
+
42
+ console.log('šŸ  Client 2 joining room: test-room');
43
+ client2.joinRoom('test-room');
44
+
45
+ await sleep(1000);
46
+
47
+ console.log('\n✨ Test complete! Both clients joined the room.');
48
+ console.log('šŸ“Š Peers in Client 1:', Array.from(client1.peers.keys()));
49
+ console.log('šŸ“Š Peers in Client 2:', Array.from(client2.peers.keys()));
50
+
51
+ // Cleanup
52
+ client1.disconnect();
53
+ client2.disconnect();
54
+
55
+ process.exit(0);
56
+ } catch (error) {
57
+ console.error('āŒ Test failed:', error.message);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ runTest();
package/wrangler.toml ADDED
@@ -0,0 +1,23 @@
1
+ name = "uniwrtc"
2
+ main = "src/index.js"
3
+ compatibility_date = "2024-12-20"
4
+
5
+ [[migrations]]
6
+ tag = "v1"
7
+ new_sqlite_classes = ["Room"]
8
+
9
+ [[durable_objects.bindings]]
10
+ name = "ROOMS"
11
+ class_name = "Room"
12
+
13
+ [env.production]
14
+ routes = [
15
+ { pattern = "signal.peer.ooo/*", zone_name = "peer.ooo" }
16
+ ]
17
+
18
+ [[env.production.durable_objects.bindings]]
19
+ name = "ROOMS"
20
+ class_name = "Room"
21
+
22
+ [build]
23
+ command = "npm install"