uniwrtc 1.4.0 → 1.5.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/CLOUDFLARE_DEPLOYMENT.md +4 -4
- package/QUICKSTART_CLOUDFLARE.md +3 -20
- package/README.md +83 -38
- package/deploy-cloudflare.bat +2 -2
- package/deploy-cloudflare.sh +2 -2
- package/package-cf.json +1 -1
- package/package.json +2 -3
- package/playwright.config.js +10 -3
- package/server.js +1 -1
- package/src/client-cloudflare.js +2 -3
- package/src/index.js +1 -2
- package/src/main.js +25 -18
- package/src/room.js +1 -1
- package/tests/e2e.spec.js +37 -60
package/CLOUDFLARE_DEPLOYMENT.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# UniWRTC Cloudflare Deployment Guide
|
|
2
2
|
|
|
3
|
-
## Deploy
|
|
3
|
+
## Deploy (Cloudflare Pages)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This repo’s current demo works client-side (Nostr), so you can deploy just the static site to Cloudflare Pages.
|
|
6
6
|
|
|
7
7
|
### Prerequisites
|
|
8
8
|
|
|
@@ -14,12 +14,12 @@ If you only want to host the demo UI (static assets) and do NOT want Durable Obj
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npm install
|
|
17
|
-
npm run deploy:cf:
|
|
17
|
+
npm run deploy:cf:pages
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Notes:
|
|
21
21
|
- This deploys the `dist/` folder (static hosting).
|
|
22
|
-
- No server routes are deployed
|
|
22
|
+
- No server routes are deployed.
|
|
23
23
|
|
|
24
24
|
## Custom Domain (signal.peer.ooo)
|
|
25
25
|
|
package/QUICKSTART_CLOUDFLARE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Quick Start - Deploy to Cloudflare
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Cloudflare Pages (static)
|
|
4
4
|
|
|
5
5
|
This repo’s current demo works client-side (Nostr), so you can deploy just the static site to Cloudflare Pages.
|
|
6
6
|
|
|
@@ -12,28 +12,11 @@ This repo’s current demo works client-side (Nostr), so you can deploy just the
|
|
|
12
12
|
### Deploy
|
|
13
13
|
```bash
|
|
14
14
|
npm install
|
|
15
|
-
npm run deploy:cf:
|
|
15
|
+
npm run deploy:cf:pages
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Wrangler will prompt you to pick/create a Pages project the first time.
|
|
19
19
|
|
|
20
|
-
## Prerequisites
|
|
21
|
-
- Cloudflare account (free tier works)
|
|
22
|
-
- Node.js installed
|
|
23
|
-
|
|
24
|
-
## Deploy
|
|
25
|
-
|
|
26
|
-
### macOS / Linux
|
|
27
|
-
```bash
|
|
28
|
-
chmod +x deploy-cloudflare.sh
|
|
29
|
-
./deploy-cloudflare.sh
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Windows
|
|
33
|
-
```bash
|
|
34
|
-
deploy-cloudflare.bat
|
|
35
|
-
```
|
|
36
|
-
|
|
37
20
|
## What this does
|
|
38
21
|
1. ✅ Builds the Vite site into `dist/`
|
|
39
22
|
2. ✅ Deploys `dist/` to Cloudflare Pages
|
|
@@ -42,4 +25,4 @@ deploy-cloudflare.bat
|
|
|
42
25
|
|
|
43
26
|
Then set your custom domain in Cloudflare Pages (and point `signal` to `<project>.pages.dev`).
|
|
44
27
|
|
|
45
|
-
That's it! Your demo is now on Cloudflare Pages
|
|
28
|
+
That's it! Your demo is now on Cloudflare Pages.
|
package/README.md
CHANGED
|
@@ -1,53 +1,88 @@
|
|
|
1
1
|
# UniWRTC
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A WebRTC demo + signaling utilities.
|
|
4
|
+
|
|
5
|
+
The default demo flow uses **Nostr relays for WebRTC signaling** (offer/answer/ICE + presence) and a **WebRTC data channel for app data**.
|
|
6
|
+
|
|
7
|
+
This repo also includes an optional **legacy HTTP polling signaling server** (useful for local development), but the live demo is now fully static/serverless.
|
|
4
8
|
|
|
5
9
|
Available on npm: https://www.npmjs.com/package/uniwrtc
|
|
6
10
|
|
|
7
11
|
## Features
|
|
8
12
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- 📊 **Health monitoring** - HTTP health check endpoint for monitoring
|
|
15
|
-
- 🎯 **Minimal dependencies** - Lightweight implementation with minimal runtime deps
|
|
13
|
+
- 🤝 **Nostr signaling (demo default)** - WebRTC offer/answer/ICE + presence over Nostr relays
|
|
14
|
+
- 📦 **WebRTC for data** - App/chat data rides the data channel (not the relay)
|
|
15
|
+
- 🏠 **Session-based rooms** - Multiple sessions with isolated peer groups
|
|
16
|
+
- 🔌 **Optional legacy server** - HTTP polling signaling server (local/dev)
|
|
17
|
+
- 🧊 **STUN-only** - No TURN in the default demo
|
|
16
18
|
|
|
17
19
|
## Quick Start
|
|
18
20
|
|
|
19
|
-
## Using with `simple-peer` (
|
|
21
|
+
## Using with `simple-peer` (Nostr signaling)
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
`simple-peer` uses `{ type, sdp }` objects, so use the adapter in [simple-peer-adapter.js](simple-peer-adapter.js).
|
|
23
|
+
The repo’s default demo uses Nostr relays for signaling. Here’s a minimal `simple-peer` example that uses the built-in Nostr client for signaling messages.
|
|
23
24
|
|
|
24
|
-
Example (browser):
|
|
25
|
+
Example (browser, two tabs):
|
|
25
26
|
|
|
26
27
|
```js
|
|
27
28
|
import Peer from 'simple-peer';
|
|
28
|
-
import
|
|
29
|
-
|
|
29
|
+
import { createNostrClient } from './src/nostr/nostrClient.js';
|
|
30
|
+
|
|
31
|
+
const relayUrl = 'wss://relay.damus.io';
|
|
32
|
+
const room = 'my-room';
|
|
33
|
+
|
|
34
|
+
const nostr = createNostrClient({
|
|
35
|
+
relayUrl,
|
|
36
|
+
room,
|
|
37
|
+
onPayload: ({ from, payload }) => {
|
|
38
|
+
// Only accept signals intended for us
|
|
39
|
+
if (payload?.type !== 'sp-signal') return;
|
|
40
|
+
if (payload?.to !== myId) return;
|
|
41
|
+
if (targetId && from !== targetId) return;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
peer.signal(payload.signal);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.warn('Failed to apply signal:', e);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
30
50
|
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
await nostr.connect();
|
|
52
|
+
|
|
53
|
+
const myId = nostr.getPublicKey();
|
|
54
|
+
console.log('My Peer ID:', myId);
|
|
33
55
|
|
|
34
|
-
//
|
|
35
|
-
|
|
56
|
+
// Set this in each tab (copy/paste from the other tab)
|
|
57
|
+
const targetId = prompt('Paste the other Peer ID:')?.trim();
|
|
58
|
+
if (!targetId) throw new Error('Missing targetId');
|
|
59
|
+
|
|
60
|
+
// Deterministic initiator prevents offer collisions
|
|
61
|
+
const initiator = myId.localeCompare(targetId) < 0;
|
|
36
62
|
|
|
37
|
-
// Ensure exactly ONE side initiates for a given pair
|
|
38
|
-
const initiator = chooseDeterministicInitiator(client.clientId, targetId);
|
|
39
63
|
const peer = new Peer({ initiator, trickle: true });
|
|
40
|
-
const cleanup = attachUniWRTCToSimplePeer(client, peer);
|
|
41
64
|
|
|
42
|
-
peer.on('signal', (signal) => {
|
|
43
|
-
//
|
|
44
|
-
|
|
65
|
+
peer.on('signal', async (signal) => {
|
|
66
|
+
// Send signaling via Nostr; WebRTC data stays P2P.
|
|
67
|
+
await nostr.send({
|
|
68
|
+
type: 'sp-signal',
|
|
69
|
+
to: targetId,
|
|
70
|
+
signal,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
peer.on('connect', () => {
|
|
75
|
+
console.log('WebRTC connected');
|
|
76
|
+
peer.send('hello over datachannel');
|
|
45
77
|
});
|
|
46
78
|
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
peer.on('data', (data) => {
|
|
80
|
+
console.log('Got data:', data.toString());
|
|
81
|
+
});
|
|
49
82
|
```
|
|
50
83
|
|
|
84
|
+
Note: Nostr relays are generally public. Don’t send secrets in signaling payloads.
|
|
85
|
+
|
|
51
86
|
### Installation
|
|
52
87
|
|
|
53
88
|
#### From npm (recommended)
|
|
@@ -85,7 +120,11 @@ PORT=8080
|
|
|
85
120
|
|
|
86
121
|
### Try the Demo
|
|
87
122
|
|
|
88
|
-
The interactive demo is available live at **https://signal.peer.ooo/** (Cloudflare
|
|
123
|
+
The interactive demo is available live at **https://signal.peer.ooo/** (Cloudflare Pages static site) or run locally.
|
|
124
|
+
|
|
125
|
+
The demo uses:
|
|
126
|
+
- Nostr relays for signaling
|
|
127
|
+
- WebRTC data channels for data/chat
|
|
89
128
|
|
|
90
129
|
**Using the deployed demo (recommended):**
|
|
91
130
|
1. Open https://signal.peer.ooo/ in two browser tabs
|
|
@@ -95,15 +134,15 @@ The interactive demo is available live at **https://signal.peer.ooo/** (Cloudfla
|
|
|
95
134
|
5. Open the P2P chat and send messages between tabs
|
|
96
135
|
|
|
97
136
|
**Or run locally:**
|
|
98
|
-
1.
|
|
99
|
-
2. Start
|
|
137
|
+
1. Install deps: `npm install`
|
|
138
|
+
2. Start Vite: `npm run dev` (demo at `http://localhost:5173/`)
|
|
100
139
|
3. Open the demo in two browser tabs
|
|
101
140
|
4. Enter the same session ID in both, then Connect
|
|
102
141
|
5. Chat P2P once data channels open
|
|
103
142
|
|
|
104
143
|
## Usage
|
|
105
144
|
|
|
106
|
-
### Server API
|
|
145
|
+
### Legacy HTTP Signaling Server API (optional)
|
|
107
146
|
|
|
108
147
|
The signaling server supports:
|
|
109
148
|
- HTTP polling signaling (no WebSockets)
|
|
@@ -342,10 +381,8 @@ client.on('ice-candidate', async (data) => {
|
|
|
342
381
|
await client.connect();
|
|
343
382
|
client.joinSession('my-video-session');
|
|
344
383
|
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
await cfClient.connect();
|
|
348
|
-
cfClient.joinSession('my-session');
|
|
384
|
+
// Note: https://signal.peer.ooo is the static demo site (Nostr signaling),
|
|
385
|
+
// not an HTTP polling signaling server endpoint.
|
|
349
386
|
```
|
|
350
387
|
|
|
351
388
|
## API Reference
|
|
@@ -407,25 +444,33 @@ Response:
|
|
|
407
444
|
|
|
408
445
|
## Architecture
|
|
409
446
|
|
|
447
|
+
### Nostr Signaling (Demo Default)
|
|
448
|
+
|
|
449
|
+
- Peers publish signaling messages (offer/answer/ICE/presence) to a Nostr relay.
|
|
450
|
+
- Messages are scoped to a room/session and targeted to a specific peer session.
|
|
451
|
+
- Once the WebRTC data channel opens, application data/chat is sent P2P.
|
|
452
|
+
- This is designed to work without running your own signaling server.
|
|
453
|
+
|
|
410
454
|
### Session-based Peer Isolation
|
|
411
455
|
|
|
412
456
|
- **Sessions**: Each session is identified by a unique string ID (also called "room" in the UI)
|
|
413
457
|
- **Peer routing**: Each peer gets a unique client ID; signaling messages are routed only to intended targets
|
|
414
458
|
- **Session isolation**: Peers in different sessions cannot see or communicate with each other
|
|
415
|
-
- **Cloudflare Durable Objects**: Uses DO state to isolate sessions; routing by `?room=` query param per session
|
|
416
459
|
- Clients join with `joinSession(sessionId)` and receive notifications when other peers join the same session
|
|
417
460
|
|
|
418
461
|
### Message Flow
|
|
419
462
|
|
|
420
|
-
|
|
421
|
-
|
|
463
|
+
This section applies to the legacy HTTP polling server:
|
|
464
|
+
|
|
465
|
+
1. Client connects via HTTP(S)
|
|
466
|
+
2. Server assigns a unique client ID
|
|
422
467
|
3. Client sends join message with session ID
|
|
423
468
|
4. Server broadcasts `peer-joined` to other peers in the same session only
|
|
424
469
|
5. Peers exchange WebRTC offers/answers/ICE candidates via the server
|
|
425
470
|
6. Server routes signaling messages to specific peers by target ID (unicast, not broadcast)
|
|
426
471
|
|
|
427
472
|
Notes:
|
|
428
|
-
-
|
|
473
|
+
- Server signaling uses JSON over HTTPS requests to `/api` (polling).
|
|
429
474
|
- Offers/answers are transmitted as SDP strings (text-only) in the `offer`/`answer` fields.
|
|
430
475
|
- ICE candidates are transmitted as a compact text string: `candidate|sdpMid|sdpMLineIndex`.
|
|
431
476
|
|
package/deploy-cloudflare.bat
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
@echo off
|
|
2
|
-
REM UniWRTC Cloudflare Automated Setup Script (Windows
|
|
2
|
+
REM UniWRTC Cloudflare Automated Setup Script (Windows)
|
|
3
3
|
REM Deploys the static demo to Cloudflare Pages.
|
|
4
4
|
|
|
5
5
|
setlocal enabledelayedexpansion
|
|
6
6
|
|
|
7
|
-
echo 🚀 UniWRTC Cloudflare Setup (Pages
|
|
7
|
+
echo 🚀 UniWRTC Cloudflare Setup (Cloudflare Pages)
|
|
8
8
|
echo ============================
|
|
9
9
|
echo.
|
|
10
10
|
|
package/deploy-cloudflare.sh
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
# UniWRTC Cloudflare Automated Setup Script
|
|
3
|
+
# UniWRTC Cloudflare Automated Setup Script
|
|
4
4
|
# Deploys the static demo to Cloudflare Pages.
|
|
5
5
|
|
|
6
6
|
set -e
|
|
7
7
|
|
|
8
|
-
echo "🚀 UniWRTC Cloudflare Setup (Pages
|
|
8
|
+
echo "🚀 UniWRTC Cloudflare Setup (Cloudflare Pages)"
|
|
9
9
|
echo "============================"
|
|
10
10
|
echo ""
|
|
11
11
|
|
package/package-cf.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniwrtc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A universal WebRTC signaling service",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
"build": "vite build",
|
|
11
11
|
"preview": "vite preview",
|
|
12
12
|
"deploy:cf:pages": "npm run build && npx wrangler pages deploy dist",
|
|
13
|
-
"deploy:cf:no-do": "npm run deploy:cf:pages",
|
|
14
13
|
"server": "node server.js",
|
|
15
|
-
"test": "
|
|
14
|
+
"test": "playwright test"
|
|
16
15
|
},
|
|
17
16
|
"keywords": [
|
|
18
17
|
"webrtc",
|
package/playwright.config.js
CHANGED
|
@@ -3,17 +3,24 @@ import { defineConfig, devices } from '@playwright/test';
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
testDir: './tests',
|
|
5
5
|
// Run tests IN PARALLEL - all browsers at the same time
|
|
6
|
-
fullyParallel:
|
|
6
|
+
fullyParallel: false,
|
|
7
7
|
forbidOnly: !!process.env.CI,
|
|
8
8
|
retries: process.env.CI ? 2 : 0,
|
|
9
|
-
workers:
|
|
9
|
+
workers: process.env.CI ? 2 : 1,
|
|
10
10
|
reporter: 'html',
|
|
11
11
|
use: {
|
|
12
|
-
baseURL: '
|
|
12
|
+
baseURL: 'http://127.0.0.1:5173',
|
|
13
13
|
trace: 'on-first-retry',
|
|
14
14
|
screenshot: 'only-on-failure',
|
|
15
15
|
},
|
|
16
16
|
|
|
17
|
+
webServer: {
|
|
18
|
+
command: 'npm run dev -- --host 127.0.0.1 --port 5173',
|
|
19
|
+
url: 'http://127.0.0.1:5173',
|
|
20
|
+
reuseExistingServer: !process.env.CI,
|
|
21
|
+
timeout: 120_000,
|
|
22
|
+
},
|
|
23
|
+
|
|
17
24
|
projects: [
|
|
18
25
|
{
|
|
19
26
|
name: 'chromium',
|
package/server.js
CHANGED
|
@@ -10,7 +10,7 @@ const __dirname = path.dirname(__filename);
|
|
|
10
10
|
|
|
11
11
|
const CLIENT_TTL_MS = 60_000;
|
|
12
12
|
|
|
13
|
-
// Top-level rooms are keyed by ?room=
|
|
13
|
+
// Top-level rooms are keyed by ?room=
|
|
14
14
|
// Each room contains multiple sessions keyed by sessionId.
|
|
15
15
|
const rooms = new Map();
|
|
16
16
|
|
package/src/client-cloudflare.js
CHANGED
|
@@ -111,7 +111,7 @@ class UniWRTCClient {
|
|
|
111
111
|
|
|
112
112
|
joinSession(sessionId) {
|
|
113
113
|
this.sessionId = sessionId;
|
|
114
|
-
//
|
|
114
|
+
// Session is provided via the room query param on connection
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
leaveSession() {
|
|
@@ -153,8 +153,7 @@ class UniWRTCClient {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
listRooms() {
|
|
156
|
-
|
|
157
|
-
console.log('Room listing not available with Durable Objects');
|
|
156
|
+
console.log('Room listing is not available for this transport');
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
on(event, handler) {
|
package/src/index.js
CHANGED
package/src/main.js
CHANGED
|
@@ -74,7 +74,7 @@ document.getElementById('app').innerHTML = `
|
|
|
74
74
|
<div class="connection-controls">
|
|
75
75
|
<div>
|
|
76
76
|
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Relay URL(s)</label>
|
|
77
|
-
<input type="text" id="relayUrl" placeholder="wss://relay.damus.io" value="${DEFAULT_RELAYS.join(', ')}" style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-family: monospace; font-size: 12px;">
|
|
77
|
+
<input type="text" id="relayUrl" data-testid="relayUrl" placeholder="wss://relay.damus.io" value="${DEFAULT_RELAYS.join(', ')}" style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-family: monospace; font-size: 12px;">
|
|
78
78
|
</div>
|
|
79
79
|
<div>
|
|
80
80
|
<label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Room / Session ID</label>
|
|
@@ -155,12 +155,20 @@ roomInput.addEventListener('input', () => {
|
|
|
155
155
|
document.getElementById('sessionId').textContent = roomInput.value.trim() || 'Not joined';
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
// STUN-only
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
// ICE servers: STUN-only by default (no TURN). For deterministic local testing,
|
|
159
|
+
// support host-only ICE via URL flag: ?ice=host (or ?ice=none)
|
|
160
|
+
const iceMode = (params.get('ice') || '').toLowerCase();
|
|
161
|
+
const ICE_SERVERS = iceMode === 'host' || iceMode === 'none'
|
|
162
|
+
? []
|
|
163
|
+
: [
|
|
164
|
+
{ urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] },
|
|
165
|
+
{ urls: ['stun:stun2.l.google.com:19302', 'stun:stun3.l.google.com:19302'] },
|
|
166
|
+
{ urls: ['stun:stun.cloudflare.com:3478'] },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
if (ICE_SERVERS.length === 0) {
|
|
170
|
+
log('Using host-only ICE candidates (no STUN)', 'info');
|
|
171
|
+
}
|
|
164
172
|
|
|
165
173
|
function log(message, type = 'info') {
|
|
166
174
|
const logContainer = document.getElementById('logContainer');
|
|
@@ -170,11 +178,11 @@ function log(message, type = 'info') {
|
|
|
170
178
|
entry.textContent = `[${timestamp}] ${message}`;
|
|
171
179
|
|
|
172
180
|
// Add testid for specific log messages
|
|
173
|
-
if (message.includes('Connected with client ID')) {
|
|
181
|
+
if (message.includes('Connected with client ID') || message.includes('Nostr connection established')) {
|
|
174
182
|
entry.setAttribute('data-testid', 'log-connected');
|
|
175
|
-
} else if (message.includes('Joined session')) {
|
|
183
|
+
} else if (message.includes('Joined session') || message.includes('Joined Nostr room')) {
|
|
176
184
|
entry.setAttribute('data-testid', 'log-joined');
|
|
177
|
-
} else if (message.includes('Peer joined')) {
|
|
185
|
+
} else if (message.includes('Peer joined') || message.includes('Peer seen')) {
|
|
178
186
|
entry.setAttribute('data-testid', 'log-peer-joined');
|
|
179
187
|
} else if (message.includes('Data channel open')) {
|
|
180
188
|
entry.setAttribute('data-testid', 'log-data-channel');
|
|
@@ -199,8 +207,7 @@ function updateStatus(connected) {
|
|
|
199
207
|
badge.className = 'status-badge status-disconnected';
|
|
200
208
|
connectBtn.disabled = false;
|
|
201
209
|
disconnectBtn.disabled = true;
|
|
202
|
-
|
|
203
|
-
document.getElementById('sessionId').textContent = 'Not joined';
|
|
210
|
+
// Keep client/session labels stable; identity and room are local state.
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
|
|
@@ -366,6 +373,11 @@ async function connectNostr() {
|
|
|
366
373
|
const relayUrlRaw = document.getElementById('relayUrl').value.trim();
|
|
367
374
|
const roomIdInput = document.getElementById('roomId');
|
|
368
375
|
const roomId = roomIdInput.value.trim();
|
|
376
|
+
|
|
377
|
+
if (!relayUrlRaw) {
|
|
378
|
+
log('Please enter a relay URL', 'error');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
369
381
|
|
|
370
382
|
const relayCandidatesRaw = relayUrlRaw.toLowerCase() === 'auto'
|
|
371
383
|
? DEFAULT_RELAYS
|
|
@@ -380,11 +392,6 @@ async function connectNostr() {
|
|
|
380
392
|
|
|
381
393
|
const relayCandidates = relayCandidatesRaw.length ? relayCandidatesRaw : DEFAULT_RELAYS;
|
|
382
394
|
|
|
383
|
-
if (relayCandidates.length === 0) {
|
|
384
|
-
log('Please enter a relay URL', 'error');
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
395
|
const effectiveRoom = roomId || `room-${Math.random().toString(36).substring(2, 10)}`;
|
|
389
396
|
if (!roomId) roomIdInput.value = effectiveRoom;
|
|
390
397
|
|
|
@@ -731,7 +738,7 @@ async function connectWebRTC() {
|
|
|
731
738
|
let finalUrl = serverUrl;
|
|
732
739
|
if (serverUrl.includes('signal.peer.ooo')) {
|
|
733
740
|
finalUrl = `${serverUrl.replace(/\/$/, '')}/ws?room=${roomId}`;
|
|
734
|
-
log(`Using
|
|
741
|
+
log(`Using hosted endpoint with session: ${roomId}`, 'info');
|
|
735
742
|
}
|
|
736
743
|
|
|
737
744
|
client = new UniWRTCClient(finalUrl, { autoReconnect: false });
|
package/src/room.js
CHANGED
package/tests/e2e.spec.js
CHANGED
|
@@ -1,38 +1,30 @@
|
|
|
1
1
|
import { test, expect } from '@playwright/test';
|
|
2
2
|
|
|
3
|
-
function failOnWebSocket(page) {
|
|
4
|
-
page.on('websocket', (ws) => {
|
|
5
|
-
throw new Error(`WebSockets are disabled, but a WebSocket was opened: ${ws.url()}`);
|
|
6
|
-
});
|
|
7
|
-
}
|
|
8
|
-
|
|
9
3
|
test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
10
|
-
const BASE_URL = 'https://signal.peer.ooo';
|
|
11
4
|
const ROOM_ID = 'test';
|
|
12
5
|
|
|
13
6
|
test.describe('Connection and Session Management', () => {
|
|
14
7
|
test('should load demo page and display UI', async ({ page }) => {
|
|
15
|
-
|
|
16
|
-
await page.goto(BASE_URL);
|
|
8
|
+
await page.goto('/');
|
|
17
9
|
|
|
18
10
|
// Check main elements exist
|
|
19
11
|
await expect(page.locator('h1')).toContainText('UniWRTC Demo');
|
|
20
12
|
await expect(page.locator('text=Connection')).toBeVisible();
|
|
21
|
-
await expect(page.getByTestId('
|
|
13
|
+
await expect(page.getByTestId('relayUrl')).toBeVisible();
|
|
22
14
|
await expect(page.getByTestId('roomId')).toHaveValue('demo-room');
|
|
23
15
|
await expect(page.getByTestId('connectBtn')).toBeVisible();
|
|
24
16
|
});
|
|
25
17
|
|
|
26
|
-
test('should connect to
|
|
27
|
-
|
|
28
|
-
await page.goto(
|
|
18
|
+
test('should connect to relay and join room', async ({ page }) => {
|
|
19
|
+
test.setTimeout(60_000);
|
|
20
|
+
await page.goto('/');
|
|
29
21
|
|
|
30
22
|
// Click connect
|
|
31
23
|
await page.getByTestId('connectBtn').click();
|
|
32
24
|
|
|
33
25
|
// Wait for connection success log
|
|
34
|
-
await expect(page.getByTestId('log-connected')).toBeVisible({ timeout:
|
|
35
|
-
await expect(page.getByTestId('log-joined')).toBeVisible({ timeout:
|
|
26
|
+
await expect(page.getByTestId('log-connected')).toBeVisible({ timeout: 30000 });
|
|
27
|
+
await expect(page.getByTestId('log-joined')).toBeVisible({ timeout: 30000 });
|
|
36
28
|
|
|
37
29
|
// Check status changed to connected
|
|
38
30
|
const badge = page.getByTestId('statusBadge');
|
|
@@ -46,12 +38,12 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
46
38
|
});
|
|
47
39
|
|
|
48
40
|
test('should handle disconnect', async ({ page }) => {
|
|
49
|
-
|
|
50
|
-
await page.goto(
|
|
41
|
+
test.setTimeout(60_000);
|
|
42
|
+
await page.goto('/');
|
|
51
43
|
|
|
52
44
|
// Connect first
|
|
53
45
|
await page.getByTestId('connectBtn').click();
|
|
54
|
-
await expect(page.getByTestId('log-connected')).toBeVisible({ timeout:
|
|
46
|
+
await expect(page.getByTestId('log-connected')).toBeVisible({ timeout: 30000 });
|
|
55
47
|
|
|
56
48
|
// Now disconnect
|
|
57
49
|
await page.getByTestId('disconnectBtn').click();
|
|
@@ -59,7 +51,6 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
59
51
|
// Check status changed back to disconnected
|
|
60
52
|
const badge = page.getByTestId('statusBadge');
|
|
61
53
|
await expect(badge).toContainText('Disconnected');
|
|
62
|
-
await expect(page.getByTestId('clientId')).toContainText('Not connected');
|
|
63
54
|
});
|
|
64
55
|
});
|
|
65
56
|
|
|
@@ -68,21 +59,18 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
68
59
|
// Open three browser contexts (simulating three users)
|
|
69
60
|
const context1 = await browser.newContext();
|
|
70
61
|
const page1 = await context1.newPage();
|
|
71
|
-
failOnWebSocket(page1);
|
|
72
62
|
|
|
73
63
|
const context2 = await browser.newContext();
|
|
74
64
|
const page2 = await context2.newPage();
|
|
75
|
-
failOnWebSocket(page2);
|
|
76
65
|
|
|
77
66
|
const context3 = await browser.newContext();
|
|
78
67
|
const page3 = await context3.newPage();
|
|
79
|
-
failOnWebSocket(page3);
|
|
80
68
|
|
|
81
69
|
try {
|
|
82
70
|
// Connect all three peers to same room
|
|
83
|
-
await page1.goto(
|
|
84
|
-
await page2.goto(
|
|
85
|
-
await page3.goto(
|
|
71
|
+
await page1.goto('/?ice=host');
|
|
72
|
+
await page2.goto('/?ice=host');
|
|
73
|
+
await page3.goto('/?ice=host');
|
|
86
74
|
|
|
87
75
|
// Use shared room ID for all three peers
|
|
88
76
|
await page1.getByTestId('roomId').fill(ROOM_ID);
|
|
@@ -127,21 +115,18 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
127
115
|
test.setTimeout(90_000);
|
|
128
116
|
const context1 = await browser.newContext();
|
|
129
117
|
const page1 = await context1.newPage();
|
|
130
|
-
failOnWebSocket(page1);
|
|
131
118
|
|
|
132
119
|
const context2 = await browser.newContext();
|
|
133
120
|
const page2 = await context2.newPage();
|
|
134
|
-
failOnWebSocket(page2);
|
|
135
121
|
|
|
136
122
|
const context3 = await browser.newContext();
|
|
137
123
|
const page3 = await context3.newPage();
|
|
138
|
-
failOnWebSocket(page3);
|
|
139
124
|
|
|
140
125
|
try {
|
|
141
126
|
// Connect all three peers
|
|
142
|
-
await page1.goto(
|
|
143
|
-
await page2.goto(
|
|
144
|
-
await page3.goto(
|
|
127
|
+
await page1.goto('/?ice=host');
|
|
128
|
+
await page2.goto('/?ice=host');
|
|
129
|
+
await page3.goto('/?ice=host');
|
|
145
130
|
|
|
146
131
|
await page1.getByTestId('roomId').fill(ROOM_ID);
|
|
147
132
|
await page2.getByTestId('roomId').fill(ROOM_ID);
|
|
@@ -172,9 +157,9 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
172
157
|
}
|
|
173
158
|
|
|
174
159
|
// Wait for data channels to open - wait for at least one data channel log on each peer with extended timeout
|
|
175
|
-
await expect(page1.getByTestId('log-data-channel').first()).toBeVisible({ timeout:
|
|
176
|
-
await expect(page2.getByTestId('log-data-channel').first()).toBeVisible({ timeout:
|
|
177
|
-
await expect(page3.getByTestId('log-data-channel').first()).toBeVisible({ timeout:
|
|
160
|
+
await expect(page1.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
|
|
161
|
+
await expect(page2.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
|
|
162
|
+
await expect(page3.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
|
|
178
163
|
|
|
179
164
|
// Wait longer to ensure all data channels are fully established
|
|
180
165
|
await page1.waitForTimeout(3000);
|
|
@@ -199,21 +184,18 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
199
184
|
test.setTimeout(90_000);
|
|
200
185
|
const context1 = await browser.newContext();
|
|
201
186
|
const page1 = await context1.newPage();
|
|
202
|
-
failOnWebSocket(page1);
|
|
203
187
|
|
|
204
188
|
const context2 = await browser.newContext();
|
|
205
189
|
const page2 = await context2.newPage();
|
|
206
|
-
failOnWebSocket(page2);
|
|
207
190
|
|
|
208
191
|
const context3 = await browser.newContext();
|
|
209
192
|
const page3 = await context3.newPage();
|
|
210
|
-
failOnWebSocket(page3);
|
|
211
193
|
|
|
212
194
|
try {
|
|
213
195
|
// Connect all three peers
|
|
214
|
-
await page1.goto(
|
|
215
|
-
await page2.goto(
|
|
216
|
-
await page3.goto(
|
|
196
|
+
await page1.goto('/?ice=host');
|
|
197
|
+
await page2.goto('/?ice=host');
|
|
198
|
+
await page3.goto('/?ice=host');
|
|
217
199
|
|
|
218
200
|
await page1.getByTestId('roomId').fill(ROOM_ID);
|
|
219
201
|
await page2.getByTestId('roomId').fill(ROOM_ID);
|
|
@@ -229,9 +211,9 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
229
211
|
await expect(page3.getByTestId('log-connected')).toBeVisible({ timeout: 10000 });
|
|
230
212
|
|
|
231
213
|
// Wait for data channels to open on all peers first with extended timeout
|
|
232
|
-
await expect(page1.getByTestId('log-data-channel').first()).toBeVisible({ timeout:
|
|
233
|
-
await expect(page2.getByTestId('log-data-channel').first()).toBeVisible({ timeout:
|
|
234
|
-
await expect(page3.getByTestId('log-data-channel').first()).toBeVisible({ timeout:
|
|
214
|
+
await expect(page1.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
|
|
215
|
+
await expect(page2.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
|
|
216
|
+
await expect(page3.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
|
|
235
217
|
|
|
236
218
|
// Give the browser a moment to fully wire up data channel handlers.
|
|
237
219
|
await page1.waitForTimeout(3000);
|
|
@@ -287,16 +269,14 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
287
269
|
test('should not connect peers from different sessions', async ({ browser }) => {
|
|
288
270
|
const context1 = await browser.newContext();
|
|
289
271
|
const page1 = await context1.newPage();
|
|
290
|
-
failOnWebSocket(page1);
|
|
291
272
|
|
|
292
273
|
const context2 = await browser.newContext();
|
|
293
274
|
const page2 = await context2.newPage();
|
|
294
|
-
failOnWebSocket(page2);
|
|
295
275
|
|
|
296
276
|
try {
|
|
297
277
|
// Connect to different rooms
|
|
298
|
-
await page1.goto(
|
|
299
|
-
await page2.goto(
|
|
278
|
+
await page1.goto('/');
|
|
279
|
+
await page2.goto('/');
|
|
300
280
|
|
|
301
281
|
// Use different unique room IDs to ensure isolation
|
|
302
282
|
const roomA = `iso-a-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
@@ -328,24 +308,21 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
|
|
|
328
308
|
});
|
|
329
309
|
|
|
330
310
|
test.describe('Error Handling', () => {
|
|
331
|
-
test('should show error when
|
|
332
|
-
|
|
333
|
-
await page.
|
|
334
|
-
|
|
335
|
-
await page.getByTestId('serverUrl').fill('');
|
|
311
|
+
test('should show error when relay URL is empty', async ({ page }) => {
|
|
312
|
+
await page.goto('/');
|
|
313
|
+
await page.getByTestId('relayUrl').fill('');
|
|
336
314
|
await page.getByTestId('connectBtn').click();
|
|
337
|
-
|
|
338
|
-
await expect(page.locator('text=Please enter a server URL')).toBeVisible({ timeout: 5000 });
|
|
315
|
+
await expect(page.locator('text=Please enter a relay URL')).toBeVisible({ timeout: 5000 });
|
|
339
316
|
});
|
|
340
317
|
|
|
341
|
-
test('should
|
|
342
|
-
|
|
343
|
-
await page.goto(
|
|
344
|
-
|
|
318
|
+
test('should auto-fill a room when room ID is empty', async ({ page }) => {
|
|
319
|
+
test.setTimeout(60_000);
|
|
320
|
+
await page.goto('/');
|
|
345
321
|
await page.getByTestId('roomId').fill('');
|
|
346
322
|
await page.getByTestId('connectBtn').click();
|
|
347
|
-
|
|
348
|
-
await expect(page.
|
|
323
|
+
await expect(page.getByTestId('log-connected')).toBeVisible({ timeout: 30000 });
|
|
324
|
+
await expect(page.getByTestId('roomId')).not.toHaveValue('');
|
|
325
|
+
await expect(page.getByTestId('sessionId')).not.toContainText('Not joined');
|
|
349
326
|
});
|
|
350
327
|
});
|
|
351
328
|
});
|