zero-query 1.1.1 → 1.2.2
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/LICENSE +21 -21
- package/README.md +2 -0
- package/cli/args.js +33 -33
- package/cli/commands/build-api.js +442 -442
- package/cli/commands/build.js +253 -247
- package/cli/commands/bundle.js +1226 -1224
- package/cli/commands/create.js +178 -121
- package/cli/commands/dev/devtools/index.js +56 -56
- package/cli/commands/dev/devtools/js/components.js +49 -49
- package/cli/commands/dev/devtools/js/core.js +423 -423
- package/cli/commands/dev/devtools/js/elements.js +421 -421
- package/cli/commands/dev/devtools/js/network.js +166 -166
- package/cli/commands/dev/devtools/js/performance.js +73 -73
- package/cli/commands/dev/devtools/js/router.js +105 -105
- package/cli/commands/dev/devtools/js/source.js +132 -132
- package/cli/commands/dev/devtools/js/stats.js +35 -35
- package/cli/commands/dev/devtools/js/tabs.js +79 -79
- package/cli/commands/dev/devtools/panel.html +95 -95
- package/cli/commands/dev/devtools/styles.css +244 -244
- package/cli/commands/dev/index.js +107 -107
- package/cli/commands/dev/logger.js +75 -75
- package/cli/commands/dev/overlay.js +858 -858
- package/cli/commands/dev/server.js +220 -220
- package/cli/commands/dev/validator.js +94 -94
- package/cli/commands/dev/watcher.js +172 -172
- package/cli/help.js +114 -112
- package/cli/index.js +52 -52
- package/cli/scaffold/default/LICENSE +21 -21
- package/cli/scaffold/default/app/app.js +207 -207
- package/cli/scaffold/default/app/components/about.js +201 -201
- package/cli/scaffold/default/app/components/api-demo.js +143 -143
- package/cli/scaffold/default/app/components/contact-card.js +231 -231
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
- package/cli/scaffold/default/app/components/counter.js +127 -127
- package/cli/scaffold/default/app/components/home.js +249 -249
- package/cli/scaffold/default/app/components/not-found.js +16 -16
- package/cli/scaffold/default/app/components/playground/playground.css +115 -115
- package/cli/scaffold/default/app/components/playground/playground.html +161 -161
- package/cli/scaffold/default/app/components/playground/playground.js +116 -116
- package/cli/scaffold/default/app/components/todos.js +225 -225
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
- package/cli/scaffold/default/app/routes.js +15 -15
- package/cli/scaffold/default/app/store.js +101 -101
- package/cli/scaffold/default/global.css +552 -552
- package/cli/scaffold/default/index.html +99 -99
- package/cli/scaffold/minimal/app/app.js +85 -85
- package/cli/scaffold/minimal/app/components/about.js +68 -68
- package/cli/scaffold/minimal/app/components/counter.js +122 -122
- package/cli/scaffold/minimal/app/components/home.js +68 -68
- package/cli/scaffold/minimal/app/components/not-found.js +16 -16
- package/cli/scaffold/minimal/app/routes.js +9 -9
- package/cli/scaffold/minimal/app/store.js +36 -36
- package/cli/scaffold/minimal/global.css +300 -300
- package/cli/scaffold/minimal/index.html +44 -44
- package/cli/scaffold/ssr/app/app.js +41 -41
- package/cli/scaffold/ssr/app/components/about.js +55 -55
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
- package/cli/scaffold/ssr/app/components/home.js +37 -37
- package/cli/scaffold/ssr/app/components/not-found.js +15 -15
- package/cli/scaffold/ssr/app/routes.js +8 -8
- package/cli/scaffold/ssr/global.css +228 -228
- package/cli/scaffold/ssr/index.html +37 -37
- package/cli/scaffold/ssr/package.json +8 -8
- package/cli/scaffold/ssr/server/data/posts.js +144 -144
- package/cli/scaffold/ssr/server/index.js +213 -213
- package/cli/scaffold/webrtc/app/app.js +11 -0
- package/cli/scaffold/webrtc/app/components/video-room.js +441 -0
- package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
- package/cli/scaffold/webrtc/global.css +250 -0
- package/cli/scaffold/webrtc/index.html +21 -0
- package/cli/scaffold/webrtc/package.json +16 -0
- package/cli/scaffold/webrtc/server/index.js +198 -0
- package/cli/utils.js +305 -287
- package/dist/API.md +661 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +10454 -6614
- package/dist/zquery.min.js +8 -631
- package/index.d.ts +570 -371
- package/index.js +311 -240
- package/package.json +77 -70
- package/src/component.js +1758 -1691
- package/src/core.js +929 -921
- package/src/diff.js +497 -497
- package/src/errors.js +209 -209
- package/src/expression.js +929 -922
- package/src/http.js +242 -242
- package/src/package.json +1 -1
- package/src/reactive.js +271 -255
- package/src/router.js +878 -843
- package/src/ssr.js +421 -418
- package/src/store.js +325 -318
- package/src/utils.js +534 -515
- package/src/webrtc/e2ee.js +351 -0
- package/src/webrtc/errors.js +116 -0
- package/src/webrtc/ice.js +301 -0
- package/src/webrtc/index.js +131 -0
- package/src/webrtc/joinToken.js +119 -0
- package/src/webrtc/observe.js +172 -0
- package/src/webrtc/peer.js +351 -0
- package/src/webrtc/reactive.js +268 -0
- package/src/webrtc/room.js +625 -0
- package/src/webrtc/sdp.js +302 -0
- package/src/webrtc/sfu/index.js +43 -0
- package/src/webrtc/sfu/livekit.js +131 -0
- package/src/webrtc/sfu/mediasoup.js +150 -0
- package/src/webrtc/signaling.js +373 -0
- package/src/webrtc/turn.js +237 -0
- package/tests/_helpers/webrtcFakes.js +289 -0
- package/tests/audit.test.js +4158 -4158
- package/tests/bench/render.bench.js +77 -0
- package/tests/cli.test.js +1136 -1103
- package/tests/compare.test.js +497 -486
- package/tests/component.test.js +4076 -3938
- package/tests/core.test.js +1926 -1910
- package/tests/dev-server.test.js +489 -489
- package/tests/diff.test.js +1416 -1416
- package/tests/docs.test.js +1664 -1650
- package/tests/electron-features.test.js +864 -864
- package/tests/errors.test.js +619 -619
- package/tests/expression.test.js +1087 -1056
- package/tests/http.test.js +693 -648
- package/tests/reactive.test.js +844 -819
- package/tests/router.test.js +2423 -2327
- package/tests/ssr.test.js +916 -870
- package/tests/store.test.js +840 -830
- package/tests/test-minifier.js +153 -153
- package/tests/test-ssr.js +27 -27
- package/tests/utils.test.js +1411 -1377
- package/tests/webrtc/e2ee.test.js +283 -0
- package/tests/webrtc/ice.test.js +202 -0
- package/tests/webrtc/joinToken.test.js +89 -0
- package/tests/webrtc/observe.test.js +111 -0
- package/tests/webrtc/peer.test.js +373 -0
- package/tests/webrtc/reactive.test.js +235 -0
- package/tests/webrtc/room.test.js +406 -0
- package/tests/webrtc/sdp.test.js +151 -0
- package/tests/webrtc/sfu-livekit.test.js +119 -0
- package/tests/webrtc/sfu.test.js +160 -0
- package/tests/webrtc/signaling.test.js +251 -0
- package/tests/webrtc/turn.test.js +256 -0
- package/types/collection.d.ts +383 -383
- package/types/component.d.ts +186 -186
- package/types/errors.d.ts +135 -135
- package/types/http.d.ts +92 -92
- package/types/misc.d.ts +201 -201
- package/types/reactive.d.ts +98 -98
- package/types/router.d.ts +190 -190
- package/types/ssr.d.ts +102 -102
- package/types/store.d.ts +146 -146
- package/types/utils.d.ts +245 -245
- package/types/webrtc.d.ts +653 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// video-room.js - Mini-Discord style room over zero-server signaling.
|
|
2
|
+
//
|
|
3
|
+
// Joins a real WebRTC mesh room via `$.webrtc.join()` against the
|
|
4
|
+
// SignalingHub running in server/index.js. The user picks a room name and
|
|
5
|
+
// joins as a viewer first - no camera, no microphone, no screen capture
|
|
6
|
+
// runs until they explicitly click a Start button. Each control acquires
|
|
7
|
+
// (or releases) exactly the media it owns, so granting "Start mic" never
|
|
8
|
+
// turns on the camera and vice-versa.
|
|
9
|
+
|
|
10
|
+
$.component('video-room', {
|
|
11
|
+
state: () => ({
|
|
12
|
+
// Pre-join form
|
|
13
|
+
roomName: 'lobby',
|
|
14
|
+
displayName: 'User-' + Math.random().toString(36).slice(2, 6),
|
|
15
|
+
// Live state
|
|
16
|
+
joined: false,
|
|
17
|
+
connecting: false,
|
|
18
|
+
status: 'Pick a room name and join. The camera and mic stay off until you turn them on.',
|
|
19
|
+
error: '',
|
|
20
|
+
// Local publishing flags
|
|
21
|
+
micOn: false,
|
|
22
|
+
camOn: false,
|
|
23
|
+
micMuted: false,
|
|
24
|
+
camMuted: false,
|
|
25
|
+
sharing: false,
|
|
26
|
+
// Live MediaStream refs (passed through reactive() unchanged because
|
|
27
|
+
// the proxy only wraps plain objects / arrays).
|
|
28
|
+
micStream: null,
|
|
29
|
+
camStream: null,
|
|
30
|
+
screenStream: null,
|
|
31
|
+
// Roster (each entry also holds the remote MediaStream for z-stream)
|
|
32
|
+
peers: [],
|
|
33
|
+
// Chat history
|
|
34
|
+
messages: [],
|
|
35
|
+
draft: '',
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
mounted() {
|
|
39
|
+
// Room/data-channel handles live on the instance; MediaStreams live
|
|
40
|
+
// in state so z-stream bindings can resolve them by name.
|
|
41
|
+
this._room = null;
|
|
42
|
+
this._chat = null;
|
|
43
|
+
this._cameraTrack = null;
|
|
44
|
+
this._unsubs = [];
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async destroyed() {
|
|
48
|
+
await this._teardown();
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// ---- Form bindings ---------------------------------------------------
|
|
52
|
+
|
|
53
|
+
setRoom(e) { this.setState({ roomName: e.target.value }); },
|
|
54
|
+
setName(e) { this.setState({ displayName: e.target.value }); },
|
|
55
|
+
setDraft(e) { this.setState({ draft: e.target.value }); },
|
|
56
|
+
|
|
57
|
+
// ---- Join / leave ----------------------------------------------------
|
|
58
|
+
|
|
59
|
+
async join(e) {
|
|
60
|
+
if (e && e.preventDefault) e.preventDefault();
|
|
61
|
+
if (this.state.joined || this.state.connecting) return;
|
|
62
|
+
if (!$.webrtc || typeof $.webrtc.join !== 'function') {
|
|
63
|
+
this.setState({ error: '$.webrtc.join is unavailable - build zquery.min.js with the webrtc bundle.' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.setState({ connecting: true, error: '', status: 'Connecting to signaling server...' });
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Pull a fresh join token + the server's preferred ws url.
|
|
71
|
+
const meta = await this._fetchJSON('/rtc/token/' + encodeURIComponent(this.state.roomName));
|
|
72
|
+
const ice = await this._fetchJSON('/rtc/turn');
|
|
73
|
+
|
|
74
|
+
this._room = await $.webrtc.join(meta.wsUrl || this._defaultWsUrl(), {
|
|
75
|
+
room: this.state.roomName,
|
|
76
|
+
token: meta.token || undefined,
|
|
77
|
+
iceServers: (ice && ice.iceServers) || undefined,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this._wireRoom(this._room);
|
|
81
|
+
|
|
82
|
+
this.setState({
|
|
83
|
+
joined: true,
|
|
84
|
+
connecting: false,
|
|
85
|
+
status: 'Joined "' + this.state.roomName + '" as a viewer. Start your mic or camera when ready.',
|
|
86
|
+
});
|
|
87
|
+
} catch (err) {
|
|
88
|
+
this.setState({
|
|
89
|
+
connecting: false,
|
|
90
|
+
error: 'Could not join: ' + (err && err.message ? err.message : String(err)),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async leave() {
|
|
96
|
+
await this._teardown();
|
|
97
|
+
this.setState({
|
|
98
|
+
joined: false,
|
|
99
|
+
connecting: false,
|
|
100
|
+
micOn: false,
|
|
101
|
+
camOn: false,
|
|
102
|
+
micMuted: false,
|
|
103
|
+
camMuted: false,
|
|
104
|
+
sharing: false,
|
|
105
|
+
micStream: null,
|
|
106
|
+
camStream: null,
|
|
107
|
+
screenStream: null,
|
|
108
|
+
peers: [],
|
|
109
|
+
messages: [],
|
|
110
|
+
status: 'Left the room. Click Join to reconnect.',
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async _teardown() {
|
|
115
|
+
for (const off of this._unsubs) { try { off(); } catch (_) {} }
|
|
116
|
+
this._unsubs = [];
|
|
117
|
+
|
|
118
|
+
if (this._room) {
|
|
119
|
+
try { await this._room.leave(); } catch (_) {}
|
|
120
|
+
this._room = null;
|
|
121
|
+
}
|
|
122
|
+
// Read raw values to avoid touching the reactive proxy while we
|
|
123
|
+
// shut things down.
|
|
124
|
+
const raw = this.state.__raw || this.state;
|
|
125
|
+
this._stopStream(raw.screenStream);
|
|
126
|
+
this._stopStream(raw.camStream);
|
|
127
|
+
this._stopStream(raw.micStream);
|
|
128
|
+
this._cameraTrack = null;
|
|
129
|
+
this._chat = null;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
_stopStream(stream) {
|
|
133
|
+
if (!stream) return;
|
|
134
|
+
for (const t of stream.getTracks()) { try { t.stop(); } catch (_) {} }
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// ---- Room wiring ----------------------------------------------------
|
|
138
|
+
|
|
139
|
+
_wireRoom(room) {
|
|
140
|
+
// Initial roster snapshot.
|
|
141
|
+
this._refreshPeers(room);
|
|
142
|
+
|
|
143
|
+
// Re-render whenever the room's peer map changes.
|
|
144
|
+
this._unsubs.push(room.peers.subscribe(() => this._refreshPeers(room)));
|
|
145
|
+
|
|
146
|
+
this._unsubs.push(room.on('peer-joined', ({ peerId }) => {
|
|
147
|
+
this.setState({ status: 'Peer joined: ' + peerId });
|
|
148
|
+
this._refreshPeers(room);
|
|
149
|
+
}));
|
|
150
|
+
this._unsubs.push(room.on('peer-left', ({ peerId }) => {
|
|
151
|
+
this.setState({ status: 'Peer left: ' + peerId });
|
|
152
|
+
this._refreshPeers(room);
|
|
153
|
+
}));
|
|
154
|
+
this._unsubs.push(room.on('error', (err) => {
|
|
155
|
+
this.setState({ error: String(err && err.message || err) });
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
// Multiplexed text-chat data channel - opens on every peer.
|
|
159
|
+
this._chat = room.dataChannel('chat');
|
|
160
|
+
this._unsubs.push(this._chat.on('message', (raw, peerId) => {
|
|
161
|
+
try {
|
|
162
|
+
const msg = JSON.parse(typeof raw === 'string' ? raw : new TextDecoder().decode(raw));
|
|
163
|
+
this._appendChat({ from: peerId, name: msg.name || peerId, text: String(msg.text || ''), mine: false });
|
|
164
|
+
} catch (_) { /* ignore malformed */ }
|
|
165
|
+
}));
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
_refreshPeers(room) {
|
|
169
|
+
const list = [];
|
|
170
|
+
const map = room.peers.peek();
|
|
171
|
+
for (const info of map.values()) {
|
|
172
|
+
list.push({ id: info.id, name: info.id, stream: info.stream });
|
|
173
|
+
}
|
|
174
|
+
this.setState({ peers: list });
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// ---- Mic ------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
async startMic() {
|
|
180
|
+
if (this.state.micStream || !this._room) return;
|
|
181
|
+
try {
|
|
182
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
183
|
+
await this._room.publish(stream);
|
|
184
|
+
this.setState({ micStream: stream, micOn: true, micMuted: false, error: '', status: 'Microphone is live.' });
|
|
185
|
+
} catch (err) {
|
|
186
|
+
this.setState({ error: 'Microphone denied or unavailable.' });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
async stopMic() {
|
|
191
|
+
const stream = this.state.micStream;
|
|
192
|
+
if (!stream || !this._room) return;
|
|
193
|
+
try { await this._room.unpublish(stream); } catch (_) {}
|
|
194
|
+
this._stopStream(stream);
|
|
195
|
+
this.setState({ micStream: null, micOn: false, micMuted: false, status: 'Microphone stopped.' });
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
toggleMute() {
|
|
199
|
+
const stream = this.state.micStream;
|
|
200
|
+
if (!stream) return;
|
|
201
|
+
const next = !this.state.micMuted;
|
|
202
|
+
for (const t of stream.getAudioTracks()) t.enabled = !next;
|
|
203
|
+
this.setState({ micMuted: next });
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// ---- Camera ---------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
async startCam() {
|
|
209
|
+
if (this.state.camStream || !this._room) return;
|
|
210
|
+
try {
|
|
211
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
212
|
+
this._cameraTrack = stream.getVideoTracks()[0] || null;
|
|
213
|
+
await this._room.publish(stream);
|
|
214
|
+
this.setState({ camStream: stream, camOn: true, camMuted: false, error: '', status: 'Camera is live.' });
|
|
215
|
+
} catch (err) {
|
|
216
|
+
this.setState({ error: 'Camera denied or unavailable.' });
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
async stopCam() {
|
|
221
|
+
const stream = this.state.camStream;
|
|
222
|
+
if (!stream || !this._room) return;
|
|
223
|
+
if (this.state.sharing) await this.stopShare();
|
|
224
|
+
try { await this._room.unpublish(stream); } catch (_) {}
|
|
225
|
+
this._stopStream(stream);
|
|
226
|
+
this._cameraTrack = null;
|
|
227
|
+
this.setState({ camStream: null, camOn: false, camMuted: false, status: 'Camera stopped.' });
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
toggleCamMute() {
|
|
231
|
+
const stream = this.state.camStream;
|
|
232
|
+
if (!stream) return;
|
|
233
|
+
const next = !this.state.camMuted;
|
|
234
|
+
for (const t of stream.getVideoTracks()) t.enabled = !next;
|
|
235
|
+
this.setState({ camMuted: next });
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
// ---- Screen share ---------------------------------------------------
|
|
239
|
+
|
|
240
|
+
async startShare() {
|
|
241
|
+
if (this.state.sharing || !this._room) return;
|
|
242
|
+
if (!navigator.mediaDevices || typeof navigator.mediaDevices.getDisplayMedia !== 'function') {
|
|
243
|
+
this.setState({ error: 'Screen capture is not supported in this browser.' });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
|
|
248
|
+
const track = stream.getVideoTracks()[0];
|
|
249
|
+
if (!track) throw new Error('No video track from getDisplayMedia');
|
|
250
|
+
// Native browser "Stop sharing" button.
|
|
251
|
+
track.onended = () => { if (this.state.sharing) this.stopShare(); };
|
|
252
|
+
await this._room.publish(stream);
|
|
253
|
+
this.setState({ screenStream: stream, sharing: true, error: '', status: 'Sharing your screen.' });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
this.setState({ error: 'Screen share denied or unavailable.' });
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
async stopShare() {
|
|
260
|
+
const stream = this.state.screenStream;
|
|
261
|
+
if (!stream) return;
|
|
262
|
+
try { await this._room.unpublish(stream); } catch (_) {}
|
|
263
|
+
this._stopStream(stream);
|
|
264
|
+
this.setState({ screenStream: null, sharing: false, status: 'Stopped sharing screen.' });
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// ---- Chat -----------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
sendChat(e) {
|
|
270
|
+
if (e && e.preventDefault) e.preventDefault();
|
|
271
|
+
const text = (this.state.draft || '').trim();
|
|
272
|
+
if (!text || !this._chat) return;
|
|
273
|
+
const payload = JSON.stringify({ name: this.state.displayName, text });
|
|
274
|
+
try { this._chat.send(payload); } catch (_) {}
|
|
275
|
+
this._appendChat({ from: 'me', name: this.state.displayName, text, mine: true });
|
|
276
|
+
this.setState({ draft: '' });
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
_appendChat(msg) {
|
|
280
|
+
const next = this.state.messages.concat([msg]);
|
|
281
|
+
if (next.length > 200) next.splice(0, next.length - 200);
|
|
282
|
+
this.setState({ messages: next });
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
// ---- Helpers --------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
async _fetchJSON(url) {
|
|
288
|
+
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
289
|
+
if (!res.ok) throw new Error(url + ' → HTTP ' + res.status);
|
|
290
|
+
return res.json();
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
_defaultWsUrl() {
|
|
294
|
+
if (typeof location === 'undefined') return 'ws://localhost:3000/rtc';
|
|
295
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
296
|
+
return proto + '//' + location.host + '/rtc';
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
// ---- Render ---------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
render() {
|
|
302
|
+
if (!this.state.joined) return this._renderLobby();
|
|
303
|
+
return this._renderRoom();
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
_renderLobby() {
|
|
307
|
+
const { roomName, displayName, status, error, connecting } = this.state;
|
|
308
|
+
return `
|
|
309
|
+
<div class="lobby">
|
|
310
|
+
<h1>zQuery WebRTC Demo</h1>
|
|
311
|
+
<p class="lead">
|
|
312
|
+
Mesh video call powered by
|
|
313
|
+
<code>$.webrtc.join()</code> against a
|
|
314
|
+
<a href="https://github.com/tonywied17/zero-server" target="_blank" rel="noopener">zero-server</a>
|
|
315
|
+
<code>SignalingHub</code>. Open this page on a second device
|
|
316
|
+
(or in another browser) and join the same room to see a peer
|
|
317
|
+
appear.
|
|
318
|
+
</p>
|
|
319
|
+
<p class="lead">
|
|
320
|
+
<strong>Camera and microphone are off by default.</strong>
|
|
321
|
+
Join the room first; then turn on the devices you actually
|
|
322
|
+
want to share.
|
|
323
|
+
</p>
|
|
324
|
+
<form class="join-form" @submit="join">
|
|
325
|
+
<label>
|
|
326
|
+
Room
|
|
327
|
+
<input type="text" value="${$.escapeHtml(roomName)}" @input="setRoom" placeholder="lobby" ${connecting ? 'disabled' : ''} />
|
|
328
|
+
</label>
|
|
329
|
+
<label>
|
|
330
|
+
Your name
|
|
331
|
+
<input type="text" value="${$.escapeHtml(displayName)}" @input="setName" placeholder="display name" ${connecting ? 'disabled' : ''} />
|
|
332
|
+
</label>
|
|
333
|
+
<button type="submit" class="primary" ${connecting ? 'disabled' : ''}>
|
|
334
|
+
${connecting ? 'Joining...' : 'Join room'}
|
|
335
|
+
</button>
|
|
336
|
+
</form>
|
|
337
|
+
<p class="status ${error ? 'error' : ''}">
|
|
338
|
+
${error ? $.escapeHtml(error) : $.escapeHtml(status)}
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
`;
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
_renderRoom() {
|
|
345
|
+
const {
|
|
346
|
+
peers, status, error, displayName, roomName,
|
|
347
|
+
micOn, camOn, micMuted, camMuted, sharing, messages, draft,
|
|
348
|
+
} = this.state;
|
|
349
|
+
|
|
350
|
+
const peerCount = peers.length + 1;
|
|
351
|
+
const peerTiles = peers.map((p, i) => `
|
|
352
|
+
<div class="tile">
|
|
353
|
+
<video z-stream="peers[${i}].stream" autoplay playsinline></video>
|
|
354
|
+
<div class="label">${$.escapeHtml(p.name || p.id)}</div>
|
|
355
|
+
</div>
|
|
356
|
+
`).join('');
|
|
357
|
+
|
|
358
|
+
const chatLines = messages.map((m) => `
|
|
359
|
+
<div class="msg ${m.mine ? 'mine' : ''}">
|
|
360
|
+
<span class="who">${$.escapeHtml(m.name)}</span>
|
|
361
|
+
<span class="text">${$.escapeHtml(m.text)}</span>
|
|
362
|
+
</div>
|
|
363
|
+
`).join('');
|
|
364
|
+
|
|
365
|
+
// Self-tile prefers the screen stream when sharing, else the camera.
|
|
366
|
+
const selfBinding = sharing ? 'screenStream' : 'camStream';
|
|
367
|
+
|
|
368
|
+
return `
|
|
369
|
+
<div class="room">
|
|
370
|
+
<aside class="sidebar">
|
|
371
|
+
<div class="room-meta">
|
|
372
|
+
<div class="room-name">#${$.escapeHtml(roomName)}</div>
|
|
373
|
+
<div class="room-sub">${peerCount} ${peerCount === 1 ? 'person' : 'people'}</div>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="roster">
|
|
376
|
+
<div class="roster-row me">
|
|
377
|
+
<span class="dot ${micOn && !micMuted ? 'on' : 'off'}"></span>
|
|
378
|
+
${$.escapeHtml(displayName)} <small>(you)</small>
|
|
379
|
+
</div>
|
|
380
|
+
${peers.map((p) => `
|
|
381
|
+
<div class="roster-row">
|
|
382
|
+
<span class="dot on"></span>
|
|
383
|
+
${$.escapeHtml(p.name || p.id)}
|
|
384
|
+
</div>
|
|
385
|
+
`).join('')}
|
|
386
|
+
</div>
|
|
387
|
+
<button class="leave" @click="leave">Leave room</button>
|
|
388
|
+
</aside>
|
|
389
|
+
|
|
390
|
+
<section class="stage">
|
|
391
|
+
<div class="tiles">
|
|
392
|
+
<div class="tile self">
|
|
393
|
+
${camOn || sharing
|
|
394
|
+
? `<video z-stream="${selfBinding}" autoplay playsinline muted></video>`
|
|
395
|
+
: '<div class="camoff">Camera off</div>'}
|
|
396
|
+
<div class="label">You${sharing ? ' · sharing' : ''}${camMuted ? ' · paused' : ''}</div>
|
|
397
|
+
</div>
|
|
398
|
+
${peerTiles}
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<div class="controls">
|
|
402
|
+
${micOn
|
|
403
|
+
? `<button class="${micMuted ? 'off' : ''}" @click="toggleMute">${micMuted ? '🔇 Unmute' : '🎤 Mute'}</button>
|
|
404
|
+
<button class="off" @click="stopMic">⏹ Stop mic</button>`
|
|
405
|
+
: `<button class="primary" @click="startMic">🎤 Start mic</button>`}
|
|
406
|
+
|
|
407
|
+
${camOn
|
|
408
|
+
? `<button class="${camMuted ? 'off' : ''}" @click="toggleCamMute">${camMuted ? '🚫 Resume' : '📷 Pause'}</button>
|
|
409
|
+
<button class="off" @click="stopCam">⏹ Stop camera</button>`
|
|
410
|
+
: `<button class="primary" @click="startCam">📷 Start camera</button>`}
|
|
411
|
+
|
|
412
|
+
${sharing
|
|
413
|
+
? `<button class="active" @click="stopShare">🛑 Stop sharing</button>`
|
|
414
|
+
: `<button @click="startShare">🖥️ Share screen</button>`}
|
|
415
|
+
|
|
416
|
+
<div class="status-inline ${error ? 'error' : ''}">
|
|
417
|
+
${error ? $.escapeHtml(error) : $.escapeHtml(status)}
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</section>
|
|
421
|
+
|
|
422
|
+
<aside class="chat">
|
|
423
|
+
<div class="chat-header">Chat</div>
|
|
424
|
+
<div class="chat-log" id="chat-log">
|
|
425
|
+
${chatLines || '<div class="empty">No messages yet. Say hi 👋</div>'}
|
|
426
|
+
</div>
|
|
427
|
+
<form class="chat-form" @submit="sendChat">
|
|
428
|
+
<input type="text" value="${$.escapeHtml(draft)}" @input="setDraft" placeholder="Message #${$.escapeHtml(roomName)}" />
|
|
429
|
+
<button type="submit" class="primary">Send</button>
|
|
430
|
+
</form>
|
|
431
|
+
</aside>
|
|
432
|
+
</div>
|
|
433
|
+
`;
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
updated() {
|
|
437
|
+
// Auto-scroll chat to the latest message after each render.
|
|
438
|
+
const log = this._el && this._el.querySelector ? this._el.querySelector('#chat-log') : null;
|
|
439
|
+
if (log) log.scrollTop = log.scrollHeight;
|
|
440
|
+
},
|
|
441
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/* {{NAME}} - WebRTC mini-room demo */
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
--bg: #0b0c10;
|
|
6
|
+
--panel: #151821;
|
|
7
|
+
--panel-2: #1c2030;
|
|
8
|
+
--border: #262b38;
|
|
9
|
+
--text: #e8ecf3;
|
|
10
|
+
--text-muted: #8a93a6;
|
|
11
|
+
--accent: #5b8def;
|
|
12
|
+
--accent-2: #16a34a;
|
|
13
|
+
--danger: #e1455c;
|
|
14
|
+
--warn: #f59e0b;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
html, body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
background: var(--bg);
|
|
21
|
+
color: var(--text);
|
|
22
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
23
|
+
height: 100vh;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body { display: flex; flex-direction: column; }
|
|
28
|
+
|
|
29
|
+
.topbar {
|
|
30
|
+
flex: 0 0 auto;
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: baseline;
|
|
33
|
+
justify-content: space-between;
|
|
34
|
+
padding: 0.6rem 1rem;
|
|
35
|
+
border-bottom: 1px solid var(--border);
|
|
36
|
+
background: var(--panel);
|
|
37
|
+
}
|
|
38
|
+
.topbar .brand { font-weight: 700; font-size: 1rem; }
|
|
39
|
+
.topbar small { color: var(--text-muted); }
|
|
40
|
+
|
|
41
|
+
main { flex: 1 1 auto; min-height: 0; }
|
|
42
|
+
|
|
43
|
+
/* ---- Lobby (pre-join) -------------------------------------------------- */
|
|
44
|
+
|
|
45
|
+
.lobby {
|
|
46
|
+
max-width: 560px;
|
|
47
|
+
margin: 4rem auto;
|
|
48
|
+
padding: 2rem;
|
|
49
|
+
background: var(--panel);
|
|
50
|
+
border: 1px solid var(--border);
|
|
51
|
+
border-radius: 0.75rem;
|
|
52
|
+
}
|
|
53
|
+
.lobby h1 { margin: 0 0 0.5rem; font-size: 1.4rem; }
|
|
54
|
+
.lobby .lead { color: var(--text-muted); line-height: 1.5; }
|
|
55
|
+
.lobby code { background: #0d101a; padding: 0.1rem 0.35rem; border-radius: 0.25rem; }
|
|
56
|
+
|
|
57
|
+
.join-form {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 0.75rem;
|
|
61
|
+
margin: 1.25rem 0;
|
|
62
|
+
}
|
|
63
|
+
.join-form label {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
gap: 0.3rem;
|
|
67
|
+
font-size: 0.85rem;
|
|
68
|
+
color: var(--text-muted);
|
|
69
|
+
}
|
|
70
|
+
.join-form input {
|
|
71
|
+
background: #0d101a;
|
|
72
|
+
color: var(--text);
|
|
73
|
+
border: 1px solid var(--border);
|
|
74
|
+
border-radius: 0.375rem;
|
|
75
|
+
padding: 0.55rem 0.75rem;
|
|
76
|
+
font-family: inherit;
|
|
77
|
+
font-size: 0.95rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
button {
|
|
81
|
+
background: var(--panel-2);
|
|
82
|
+
color: var(--text);
|
|
83
|
+
border: 1px solid var(--border);
|
|
84
|
+
border-radius: 0.375rem;
|
|
85
|
+
padding: 0.5rem 0.9rem;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
font-family: inherit;
|
|
89
|
+
}
|
|
90
|
+
button:hover { background: #232939; }
|
|
91
|
+
button.primary { background: var(--accent); border-color: transparent; color: #fff; }
|
|
92
|
+
button.primary:hover { filter: brightness(1.1); }
|
|
93
|
+
button.leave { background: var(--danger); border-color: transparent; color: #fff; }
|
|
94
|
+
button.off { background: #3a2030; color: #f3b6c2; }
|
|
95
|
+
button.active { background: var(--accent-2); border-color: transparent; color: #fff; }
|
|
96
|
+
|
|
97
|
+
.status { color: var(--text-muted); font-size: 0.9rem; margin: 0.5rem 0 0; }
|
|
98
|
+
.status.error { color: var(--danger); }
|
|
99
|
+
|
|
100
|
+
/* ---- Room layout (in-call) -------------------------------------------- */
|
|
101
|
+
|
|
102
|
+
.room {
|
|
103
|
+
display: grid;
|
|
104
|
+
grid-template-columns: 220px 1fr 280px;
|
|
105
|
+
height: 100%;
|
|
106
|
+
min-height: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.sidebar {
|
|
110
|
+
background: var(--panel);
|
|
111
|
+
border-right: 1px solid var(--border);
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
padding: 0.75rem;
|
|
115
|
+
min-height: 0;
|
|
116
|
+
}
|
|
117
|
+
.room-meta { padding: 0.25rem 0.5rem 0.75rem; border-bottom: 1px solid var(--border); }
|
|
118
|
+
.room-name { font-weight: 700; font-size: 1.05rem; }
|
|
119
|
+
.room-sub { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.15rem; }
|
|
120
|
+
|
|
121
|
+
.roster { flex: 1 1 auto; overflow-y: auto; margin-top: 0.5rem; }
|
|
122
|
+
.roster-row {
|
|
123
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
124
|
+
padding: 0.4rem 0.5rem;
|
|
125
|
+
border-radius: 0.375rem;
|
|
126
|
+
font-size: 0.9rem;
|
|
127
|
+
}
|
|
128
|
+
.roster-row.me { color: var(--accent); }
|
|
129
|
+
.roster-row small { color: var(--text-muted); font-weight: normal; }
|
|
130
|
+
.roster-row .dot {
|
|
131
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
132
|
+
background: var(--accent-2);
|
|
133
|
+
flex: 0 0 auto;
|
|
134
|
+
}
|
|
135
|
+
.roster-row .dot.off { background: var(--text-muted); }
|
|
136
|
+
|
|
137
|
+
.sidebar .leave { margin-top: 0.5rem; }
|
|
138
|
+
|
|
139
|
+
/* ---- Stage (videos + controls) ---------------------------------------- */
|
|
140
|
+
|
|
141
|
+
.stage {
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
min-height: 0;
|
|
145
|
+
background: var(--bg);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.tiles {
|
|
149
|
+
flex: 1 1 auto;
|
|
150
|
+
min-height: 0;
|
|
151
|
+
overflow-y: auto;
|
|
152
|
+
padding: 0.75rem;
|
|
153
|
+
display: grid;
|
|
154
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
155
|
+
grid-auto-rows: minmax(180px, auto);
|
|
156
|
+
gap: 0.75rem;
|
|
157
|
+
}
|
|
158
|
+
.tile {
|
|
159
|
+
background: #000;
|
|
160
|
+
border: 1px solid var(--border);
|
|
161
|
+
border-radius: 0.5rem;
|
|
162
|
+
overflow: hidden;
|
|
163
|
+
aspect-ratio: 16 / 9;
|
|
164
|
+
position: relative;
|
|
165
|
+
}
|
|
166
|
+
.tile.self { border-color: var(--accent); }
|
|
167
|
+
.tile video { width: 100%; height: 100%; object-fit: cover; background: #000; }
|
|
168
|
+
.tile .label {
|
|
169
|
+
position: absolute;
|
|
170
|
+
left: 0.5rem; bottom: 0.5rem;
|
|
171
|
+
background: rgba(0, 0, 0, 0.65);
|
|
172
|
+
padding: 0.15rem 0.5rem;
|
|
173
|
+
border-radius: 0.25rem;
|
|
174
|
+
font-size: 0.8rem;
|
|
175
|
+
}
|
|
176
|
+
.tile .camoff {
|
|
177
|
+
position: absolute; inset: 0;
|
|
178
|
+
display: flex; align-items: center; justify-content: center;
|
|
179
|
+
background: #1a1d28;
|
|
180
|
+
color: var(--text-muted);
|
|
181
|
+
font-size: 0.95rem;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.controls {
|
|
185
|
+
flex: 0 0 auto;
|
|
186
|
+
display: flex; gap: 0.5rem; align-items: center;
|
|
187
|
+
padding: 0.75rem;
|
|
188
|
+
background: var(--panel);
|
|
189
|
+
border-top: 1px solid var(--border);
|
|
190
|
+
}
|
|
191
|
+
.controls .status-inline {
|
|
192
|
+
margin-left: auto;
|
|
193
|
+
color: var(--text-muted);
|
|
194
|
+
font-size: 0.85rem;
|
|
195
|
+
text-align: right;
|
|
196
|
+
max-width: 50%;
|
|
197
|
+
}
|
|
198
|
+
.controls .status-inline.error { color: var(--danger); }
|
|
199
|
+
|
|
200
|
+
/* ---- Chat ------------------------------------------------------------- */
|
|
201
|
+
|
|
202
|
+
.chat {
|
|
203
|
+
background: var(--panel);
|
|
204
|
+
border-left: 1px solid var(--border);
|
|
205
|
+
display: flex;
|
|
206
|
+
flex-direction: column;
|
|
207
|
+
min-height: 0;
|
|
208
|
+
}
|
|
209
|
+
.chat-header {
|
|
210
|
+
padding: 0.75rem 1rem;
|
|
211
|
+
border-bottom: 1px solid var(--border);
|
|
212
|
+
font-weight: 600;
|
|
213
|
+
}
|
|
214
|
+
.chat-log {
|
|
215
|
+
flex: 1 1 auto;
|
|
216
|
+
overflow-y: auto;
|
|
217
|
+
padding: 0.75rem;
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: 0.5rem;
|
|
221
|
+
}
|
|
222
|
+
.chat-log .empty { color: var(--text-muted); font-size: 0.9rem; text-align: center; margin: auto 0; }
|
|
223
|
+
.msg .who { color: var(--accent); font-weight: 600; margin-right: 0.4rem; font-size: 0.85rem; }
|
|
224
|
+
.msg.mine .who { color: var(--accent-2); }
|
|
225
|
+
.msg .text { word-wrap: break-word; }
|
|
226
|
+
|
|
227
|
+
.chat-form {
|
|
228
|
+
flex: 0 0 auto;
|
|
229
|
+
display: flex;
|
|
230
|
+
gap: 0.4rem;
|
|
231
|
+
padding: 0.6rem;
|
|
232
|
+
border-top: 1px solid var(--border);
|
|
233
|
+
}
|
|
234
|
+
.chat-form input {
|
|
235
|
+
flex: 1 1 auto;
|
|
236
|
+
background: #0d101a;
|
|
237
|
+
color: var(--text);
|
|
238
|
+
border: 1px solid var(--border);
|
|
239
|
+
border-radius: 0.375rem;
|
|
240
|
+
padding: 0.45rem 0.6rem;
|
|
241
|
+
font-family: inherit;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* ---- Responsive tweaks ------------------------------------------------- */
|
|
245
|
+
|
|
246
|
+
@media (max-width: 900px) {
|
|
247
|
+
.room { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
|
|
248
|
+
.sidebar, .chat { border: none; border-bottom: 1px solid var(--border); }
|
|
249
|
+
.chat { max-height: 40vh; }
|
|
250
|
+
}
|