uniwrtc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/CLOUDFLARE_DEPLOYMENT.md +127 -0
- package/LICENSE +21 -0
- package/QUICKSTART_CLOUDFLARE.md +55 -0
- package/README.md +384 -0
- package/client-browser.js +249 -0
- package/client.js +231 -0
- package/demo.html +731 -0
- package/deploy-cloudflare.bat +113 -0
- package/deploy-cloudflare.sh +100 -0
- package/package-cf.json +16 -0
- package/package.json +26 -0
- package/server.js +350 -0
- package/src/client-cloudflare.js +247 -0
- package/src/index.js +35 -0
- package/src/room.js +211 -0
- package/test.js +62 -0
- package/wrangler.toml +23 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM UniWRTC Cloudflare Automated Setup Script (Windows)
|
|
3
|
+
|
|
4
|
+
setlocal enabledelayedexpansion
|
|
5
|
+
|
|
6
|
+
echo ๐ UniWRTC Cloudflare Setup
|
|
7
|
+
echo ============================
|
|
8
|
+
echo.
|
|
9
|
+
|
|
10
|
+
REM Check Node.js
|
|
11
|
+
where node >nul 2>nul
|
|
12
|
+
if errorlevel 1 (
|
|
13
|
+
echo โ Node.js not found. Please install Node.js v16+
|
|
14
|
+
exit /b 1
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
REM Check/Install Wrangler
|
|
18
|
+
where wrangler >nul 2>nul
|
|
19
|
+
if errorlevel 1 (
|
|
20
|
+
echo ๐ฆ Installing Wrangler CLI...
|
|
21
|
+
call npm install -g wrangler
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
echo โ
Prerequisites OK
|
|
25
|
+
echo.
|
|
26
|
+
|
|
27
|
+
REM Install dependencies
|
|
28
|
+
echo ๐ฆ Installing dependencies...
|
|
29
|
+
call npm install
|
|
30
|
+
echo โ
Dependencies installed
|
|
31
|
+
echo.
|
|
32
|
+
|
|
33
|
+
REM Check authentication
|
|
34
|
+
echo ๐ Checking Cloudflare authentication...
|
|
35
|
+
call wrangler whoami >nul 2>nul
|
|
36
|
+
if errorlevel 1 (
|
|
37
|
+
echo โ ๏ธ Not logged in to Cloudflare. Running login...
|
|
38
|
+
call wrangler login
|
|
39
|
+
)
|
|
40
|
+
echo โ
Authenticated with Cloudflare
|
|
41
|
+
echo.
|
|
42
|
+
|
|
43
|
+
REM Ask for domain
|
|
44
|
+
echo ๐ Domain Configuration
|
|
45
|
+
echo =====================
|
|
46
|
+
set /p DOMAIN="Enter your Cloudflare domain (e.g., peer.ooo): "
|
|
47
|
+
set /p SUBDOMAIN="Enter subdomain for signaling (e.g., signal): "
|
|
48
|
+
|
|
49
|
+
if "!DOMAIN!"=="" (
|
|
50
|
+
echo โ Domain required
|
|
51
|
+
exit /b 1
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if "!SUBDOMAIN!"=="" (
|
|
55
|
+
echo โ Subdomain required
|
|
56
|
+
exit /b 1
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
set FULL_DOMAIN=!SUBDOMAIN!.!DOMAIN!
|
|
60
|
+
|
|
61
|
+
REM Update wrangler.toml
|
|
62
|
+
echo ๐ Updating wrangler.toml...
|
|
63
|
+
(
|
|
64
|
+
echo name = "uniwrtc"
|
|
65
|
+
echo main = "src/index.js"
|
|
66
|
+
echo compatibility_date = "2024-12-20"
|
|
67
|
+
echo.
|
|
68
|
+
echo [env.production]
|
|
69
|
+
echo routes = [
|
|
70
|
+
echo { pattern = "!FULL_DOMAIN!/*", zone_name = "!DOMAIN!" }
|
|
71
|
+
echo ]
|
|
72
|
+
echo.
|
|
73
|
+
echo [[durable_objects.bindings]]
|
|
74
|
+
echo name = "ROOMS"
|
|
75
|
+
echo class_name = "Room"
|
|
76
|
+
echo.
|
|
77
|
+
echo [durable_objects]
|
|
78
|
+
echo migrations = [
|
|
79
|
+
echo { tag = "v1", new_classes = ["Room"] }
|
|
80
|
+
echo ]
|
|
81
|
+
echo.
|
|
82
|
+
echo [build]
|
|
83
|
+
echo command = "npm install"
|
|
84
|
+
) > wrangler.toml
|
|
85
|
+
|
|
86
|
+
echo โ
wrangler.toml updated
|
|
87
|
+
echo.
|
|
88
|
+
|
|
89
|
+
REM Deploy
|
|
90
|
+
echo ๐ Deploying to Cloudflare...
|
|
91
|
+
echo.
|
|
92
|
+
call wrangler deploy --env production
|
|
93
|
+
|
|
94
|
+
echo.
|
|
95
|
+
echo โ
Deployment Complete!
|
|
96
|
+
echo.
|
|
97
|
+
echo ๐ Your UniWRTC signaling server is live at:
|
|
98
|
+
echo https://!FULL_DOMAIN!/
|
|
99
|
+
echo.
|
|
100
|
+
echo ๐ Test it:
|
|
101
|
+
echo curl https://!FULL_DOMAIN!/health
|
|
102
|
+
echo.
|
|
103
|
+
echo ๐งช Local testing:
|
|
104
|
+
echo wrangler dev
|
|
105
|
+
echo.
|
|
106
|
+
echo ๐ View logs:
|
|
107
|
+
echo wrangler tail --env production
|
|
108
|
+
echo.
|
|
109
|
+
echo ๐ ๏ธ Next: Update demo.html to use:
|
|
110
|
+
echo const serverUrl = 'https://!FULL_DOMAIN!/';
|
|
111
|
+
echo.
|
|
112
|
+
|
|
113
|
+
endlocal
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# UniWRTC Cloudflare Automated Setup Script
|
|
4
|
+
# Run this to setup and deploy to Cloudflare
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
echo "๐ UniWRTC Cloudflare Setup"
|
|
9
|
+
echo "============================"
|
|
10
|
+
echo ""
|
|
11
|
+
|
|
12
|
+
# Check prerequisites
|
|
13
|
+
echo "๐ Checking prerequisites..."
|
|
14
|
+
|
|
15
|
+
if ! command -v node &> /dev/null; then
|
|
16
|
+
echo "โ Node.js not found. Please install Node.js v16+"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
if ! command -v wrangler &> /dev/null; then
|
|
21
|
+
echo "๐ฆ Installing Wrangler CLI..."
|
|
22
|
+
npm install -g wrangler
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo "โ
Prerequisites OK"
|
|
26
|
+
echo ""
|
|
27
|
+
|
|
28
|
+
# Install dependencies
|
|
29
|
+
echo "๐ฆ Installing dependencies..."
|
|
30
|
+
cd "$(dirname "$0")"
|
|
31
|
+
npm install || true
|
|
32
|
+
echo "โ
Dependencies installed"
|
|
33
|
+
echo ""
|
|
34
|
+
|
|
35
|
+
# Check Cloudflare authentication
|
|
36
|
+
echo "๐ Checking Cloudflare authentication..."
|
|
37
|
+
if ! wrangler whoami &> /dev/null; then
|
|
38
|
+
echo "โ ๏ธ Not logged in to Cloudflare. Running login..."
|
|
39
|
+
wrangler login
|
|
40
|
+
fi
|
|
41
|
+
echo "โ
Authenticated with Cloudflare"
|
|
42
|
+
echo ""
|
|
43
|
+
|
|
44
|
+
# Domain configuration
|
|
45
|
+
DOMAIN="peer.ooo"
|
|
46
|
+
SUBDOMAIN="signal"
|
|
47
|
+
|
|
48
|
+
FULL_DOMAIN="${SUBDOMAIN}.${DOMAIN}"
|
|
49
|
+
|
|
50
|
+
echo "๐ Updating wrangler.toml..."
|
|
51
|
+
cat > wrangler.toml << EOF
|
|
52
|
+
name = "uniwrtc"
|
|
53
|
+
main = "src/index.js"
|
|
54
|
+
compatibility_date = "2024-12-20"
|
|
55
|
+
|
|
56
|
+
[env.production]
|
|
57
|
+
routes = [
|
|
58
|
+
{ pattern = "${FULL_DOMAIN}/*", zone_name = "${DOMAIN}" }
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[[durable_objects.bindings]]
|
|
62
|
+
name = "ROOMS"
|
|
63
|
+
class_name = "Room"
|
|
64
|
+
|
|
65
|
+
[durable_objects]
|
|
66
|
+
migrations = [
|
|
67
|
+
{ tag = "v1", new_classes = ["Room"] }
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[build]
|
|
71
|
+
command = "npm install"
|
|
72
|
+
EOF
|
|
73
|
+
|
|
74
|
+
echo "โ
wrangler.toml updated"
|
|
75
|
+
echo ""
|
|
76
|
+
|
|
77
|
+
# Deploy
|
|
78
|
+
echo "๐ Deploying to Cloudflare..."
|
|
79
|
+
echo ""
|
|
80
|
+
echo "Deploying to production..."
|
|
81
|
+
wrangler deploy --env production
|
|
82
|
+
|
|
83
|
+
echo ""
|
|
84
|
+
echo "โ
Deployment Complete!"
|
|
85
|
+
echo ""
|
|
86
|
+
echo "๐ Your UniWRTC signaling server is live at:"
|
|
87
|
+
echo " https://${FULL_DOMAIN}/"
|
|
88
|
+
echo ""
|
|
89
|
+
echo "๐ Test it:"
|
|
90
|
+
echo " curl https://${FULL_DOMAIN}/health"
|
|
91
|
+
echo ""
|
|
92
|
+
echo "๐งช Local testing:"
|
|
93
|
+
echo " wrangler dev"
|
|
94
|
+
echo ""
|
|
95
|
+
echo "๐ View logs:"
|
|
96
|
+
echo " wrangler tail --env production"
|
|
97
|
+
echo ""
|
|
98
|
+
echo "๐ ๏ธ Next: Update demo.html to use:"
|
|
99
|
+
echo " const serverUrl = 'https://${FULL_DOMAIN}/';"
|
|
100
|
+
echo ""
|
package/package-cf.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uniwrtc-cloudflare",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "UniWRTC Cloudflare Workers deployment",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler publish",
|
|
9
|
+
"deploy-prod": "wrangler publish --env production",
|
|
10
|
+
"tail": "wrangler tail",
|
|
11
|
+
"tail-prod": "wrangler tail --env production"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"wrangler": "^3.26.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uniwrtc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A universal WebRTC signaling service",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node server.js",
|
|
8
|
+
"dev": "node server.js",
|
|
9
|
+
"test": "node test.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"webrtc",
|
|
13
|
+
"signaling",
|
|
14
|
+
"websocket",
|
|
15
|
+
"peer-to-peer",
|
|
16
|
+
"real-time"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"ws": "^8.17.1"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=14.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PORT = process.env.PORT || 8080;
|
|
7
|
+
|
|
8
|
+
// Create HTTP server
|
|
9
|
+
const server = http.createServer((req, res) => {
|
|
10
|
+
if (req.url === '/health') {
|
|
11
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
12
|
+
res.end(JSON.stringify({ status: 'ok', connections: wss.clients.size }));
|
|
13
|
+
} else if (req.url === '/' || req.url === '/demo.html') {
|
|
14
|
+
const filePath = path.join(__dirname, 'demo.html');
|
|
15
|
+
fs.readFile(filePath, 'utf-8', (err, data) => {
|
|
16
|
+
if (err) {
|
|
17
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
18
|
+
res.end('Demo not found');
|
|
19
|
+
} else {
|
|
20
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
21
|
+
res.end(data);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
} else if (req.url === '/client-browser.js') {
|
|
25
|
+
const filePath = path.join(__dirname, 'client-browser.js');
|
|
26
|
+
fs.readFile(filePath, 'utf-8', (err, data) => {
|
|
27
|
+
if (err) {
|
|
28
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
29
|
+
res.end('Client not found');
|
|
30
|
+
} else {
|
|
31
|
+
res.writeHead(200, {
|
|
32
|
+
'Content-Type': 'application/javascript',
|
|
33
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
34
|
+
'Pragma': 'no-cache',
|
|
35
|
+
'Expires': '0'
|
|
36
|
+
});
|
|
37
|
+
res.end(data);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
42
|
+
res.end('UniWRTC Signaling Server');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Create WebSocket server
|
|
47
|
+
const wss = new WebSocket.Server({ server });
|
|
48
|
+
|
|
49
|
+
// Store rooms and their participants
|
|
50
|
+
const rooms = new Map();
|
|
51
|
+
|
|
52
|
+
function log(message, data = '') {
|
|
53
|
+
const timestamp = new Date().toISOString();
|
|
54
|
+
console.log(`[${timestamp}] ${message}`, data);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function broadcastToRoom(roomId, message, excludeClient = null) {
|
|
58
|
+
const room = rooms.get(roomId);
|
|
59
|
+
if (!room) return;
|
|
60
|
+
|
|
61
|
+
room.clients.forEach(client => {
|
|
62
|
+
if (client !== excludeClient && client.readyState === WebSocket.OPEN) {
|
|
63
|
+
client.send(JSON.stringify(message));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sendToClient(client, message) {
|
|
69
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
70
|
+
client.send(JSON.stringify(message));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
wss.on('connection', (ws) => {
|
|
75
|
+
log('New connection established');
|
|
76
|
+
|
|
77
|
+
ws.clientId = generateClientId();
|
|
78
|
+
ws.room = null;
|
|
79
|
+
|
|
80
|
+
ws.on('message', (data) => {
|
|
81
|
+
try {
|
|
82
|
+
const message = JSON.parse(data);
|
|
83
|
+
log('Message received:', message.type);
|
|
84
|
+
|
|
85
|
+
switch (message.type) {
|
|
86
|
+
case 'set-id':
|
|
87
|
+
handleSetId(ws, message);
|
|
88
|
+
break;
|
|
89
|
+
case 'join':
|
|
90
|
+
handleJoin(ws, message);
|
|
91
|
+
break;
|
|
92
|
+
case 'leave':
|
|
93
|
+
handleLeave(ws, message);
|
|
94
|
+
break;
|
|
95
|
+
case 'offer':
|
|
96
|
+
case 'answer':
|
|
97
|
+
case 'ice-candidate':
|
|
98
|
+
handleSignaling(ws, message);
|
|
99
|
+
break;
|
|
100
|
+
case 'chat':
|
|
101
|
+
handleChat(ws, message);
|
|
102
|
+
break;
|
|
103
|
+
case 'list-rooms':
|
|
104
|
+
handleListRooms(ws);
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
sendToClient(ws, { type: 'error', message: 'Unknown message type' });
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
log('Error processing message:', error.message);
|
|
111
|
+
sendToClient(ws, { type: 'error', message: 'Invalid message format' });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
ws.on('close', () => {
|
|
116
|
+
log('Connection closed', ws.clientId);
|
|
117
|
+
if (ws.room) {
|
|
118
|
+
handleLeave(ws, { roomId: ws.room });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
ws.on('error', (error) => {
|
|
123
|
+
log('WebSocket error:', error.message);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Send welcome message
|
|
127
|
+
sendToClient(ws, {
|
|
128
|
+
type: 'welcome',
|
|
129
|
+
clientId: ws.clientId,
|
|
130
|
+
message: 'Connected to UniWRTC signaling server'
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function handleSetId(ws, message) {
|
|
135
|
+
const { customId } = message;
|
|
136
|
+
|
|
137
|
+
if (!customId || customId.length < 3 || customId.length > 20) {
|
|
138
|
+
sendToClient(ws, { type: 'error', message: 'Custom ID must be between 3-20 characters' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if ID is already taken
|
|
143
|
+
let idTaken = false;
|
|
144
|
+
wss.clients.forEach(client => {
|
|
145
|
+
if (client.clientId === customId && client !== ws) {
|
|
146
|
+
idTaken = true;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (idTaken) {
|
|
151
|
+
sendToClient(ws, { type: 'error', message: 'Peer ID already taken' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log(`Client ${ws.clientId} changed ID to ${customId}`);
|
|
156
|
+
ws.clientId = customId;
|
|
157
|
+
|
|
158
|
+
// Send updated welcome message
|
|
159
|
+
sendToClient(ws, {
|
|
160
|
+
type: 'welcome',
|
|
161
|
+
clientId: ws.clientId,
|
|
162
|
+
message: 'Custom peer ID set'
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleJoin(ws, message) {
|
|
167
|
+
const { roomId } = message;
|
|
168
|
+
|
|
169
|
+
if (!roomId) {
|
|
170
|
+
sendToClient(ws, { type: 'error', message: 'Room ID is required' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Leave current room if in one
|
|
175
|
+
if (ws.room) {
|
|
176
|
+
handleLeave(ws, { roomId: ws.room });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Create room if it doesn't exist
|
|
180
|
+
if (!rooms.has(roomId)) {
|
|
181
|
+
rooms.set(roomId, {
|
|
182
|
+
id: roomId,
|
|
183
|
+
clients: new Set(),
|
|
184
|
+
createdAt: Date.now()
|
|
185
|
+
});
|
|
186
|
+
log('Room created:', roomId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const room = rooms.get(roomId);
|
|
190
|
+
room.clients.add(ws);
|
|
191
|
+
ws.room = roomId;
|
|
192
|
+
|
|
193
|
+
log(`Client ${ws.clientId} joined room ${roomId}`);
|
|
194
|
+
|
|
195
|
+
// Get list of existing clients in the room
|
|
196
|
+
const existingClients = Array.from(room.clients)
|
|
197
|
+
.filter(client => client !== ws)
|
|
198
|
+
.map(client => client.clientId);
|
|
199
|
+
|
|
200
|
+
// Notify the joining client
|
|
201
|
+
sendToClient(ws, {
|
|
202
|
+
type: 'joined',
|
|
203
|
+
roomId: roomId,
|
|
204
|
+
clientId: ws.clientId,
|
|
205
|
+
clients: existingClients
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Notify other clients in the room
|
|
209
|
+
broadcastToRoom(roomId, {
|
|
210
|
+
type: 'peer-joined',
|
|
211
|
+
peerId: ws.clientId,
|
|
212
|
+
clientId: ws.clientId
|
|
213
|
+
}, ws);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function handleLeave(ws, message) {
|
|
217
|
+
const { roomId } = message;
|
|
218
|
+
|
|
219
|
+
if (!roomId || !rooms.has(roomId)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const room = rooms.get(roomId);
|
|
224
|
+
room.clients.delete(ws);
|
|
225
|
+
|
|
226
|
+
log(`Client ${ws.clientId} left room ${roomId}`);
|
|
227
|
+
|
|
228
|
+
// Notify other clients
|
|
229
|
+
broadcastToRoom(roomId, {
|
|
230
|
+
type: 'peer-left',
|
|
231
|
+
clientId: ws.clientId
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Clean up empty rooms
|
|
235
|
+
if (room.clients.size === 0) {
|
|
236
|
+
rooms.delete(roomId);
|
|
237
|
+
log('Room deleted:', roomId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
ws.room = null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function handleSignaling(ws, message) {
|
|
244
|
+
const { targetId, roomId } = message;
|
|
245
|
+
|
|
246
|
+
if (!ws.room) {
|
|
247
|
+
sendToClient(ws, { type: 'error', message: 'Not in a room' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!targetId) {
|
|
252
|
+
// Broadcast to all in room if no specific target
|
|
253
|
+
broadcastToRoom(ws.room, {
|
|
254
|
+
...message,
|
|
255
|
+
peerId: ws.clientId
|
|
256
|
+
}, ws);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Send to specific client
|
|
261
|
+
const room = rooms.get(ws.room);
|
|
262
|
+
if (!room) return;
|
|
263
|
+
|
|
264
|
+
for (const client of room.clients) {
|
|
265
|
+
if (client.clientId === targetId) {
|
|
266
|
+
sendToClient(client, {
|
|
267
|
+
...message,
|
|
268
|
+
peerId: ws.clientId
|
|
269
|
+
});
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function handleChat(ws, message) {
|
|
276
|
+
const { roomId, text } = message;
|
|
277
|
+
|
|
278
|
+
if (!roomId || !text) {
|
|
279
|
+
sendToClient(ws, { type: 'error', message: 'Room ID and text are required' });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
log(`Chat message in room ${roomId}: ${text.substring(0, 50)}`);
|
|
284
|
+
|
|
285
|
+
const room = rooms.get(roomId);
|
|
286
|
+
if (!room) {
|
|
287
|
+
sendToClient(ws, { type: 'error', message: 'Room not found' });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Broadcast chat to ALL clients in the room (including sender)
|
|
292
|
+
room.clients.forEach(client => {
|
|
293
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
294
|
+
client.send(JSON.stringify({
|
|
295
|
+
type: 'chat',
|
|
296
|
+
text: text,
|
|
297
|
+
senderId: ws.clientId,
|
|
298
|
+
roomId: roomId
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function handleListRooms(ws) {
|
|
305
|
+
const roomList = Array.from(rooms.entries()).map(([id, room]) => ({
|
|
306
|
+
id,
|
|
307
|
+
clients: room.clients.size,
|
|
308
|
+
createdAt: room.createdAt
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
sendToClient(ws, {
|
|
312
|
+
type: 'room-list',
|
|
313
|
+
rooms: roomList
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function generateClientId() {
|
|
318
|
+
return Math.random().toString(36).substring(2, 11);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
server.listen(PORT, () => {
|
|
322
|
+
log(`UniWRTC Signaling Server listening on port ${PORT}`);
|
|
323
|
+
console.log(`\n>>> Demo available at: http://localhost:${PORT}\n`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Graceful shutdown
|
|
327
|
+
process.on('SIGTERM', () => {
|
|
328
|
+
log('SIGTERM received, closing server...');
|
|
329
|
+
server.close(() => {
|
|
330
|
+
log('Server closed');
|
|
331
|
+
process.exit(0);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
process.on('SIGINT', () => {
|
|
336
|
+
log('SIGINT received, closing server...');
|
|
337
|
+
// Close all WebSocket connections
|
|
338
|
+
wss.clients.forEach(client => {
|
|
339
|
+
client.close();
|
|
340
|
+
});
|
|
341
|
+
server.close(() => {
|
|
342
|
+
log('Server closed');
|
|
343
|
+
process.exit(0);
|
|
344
|
+
});
|
|
345
|
+
// Force exit after 5 seconds if still hanging
|
|
346
|
+
setTimeout(() => {
|
|
347
|
+
log('Forced shutdown');
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}, 5000);
|
|
350
|
+
});
|