accelr8 3.8.2 → 4.0.0-next.1
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/README.md +298 -64
- package/dist/chunks/accounts-CCKXHSQQ.js +54 -0
- package/dist/chunks/{agentcli-ZI2O7ID7.js → agentcli-HL6V26E7.js} +6 -3
- package/dist/chunks/{app-SYLCS3QX.js → app-JMUXGMWK.js} +158 -21
- package/dist/chunks/authclient-AUBBFWCI.js +34 -0
- package/dist/chunks/authpage-X2AKCMVH.js +21 -0
- package/dist/chunks/chunk-3OTVRBTJ.js +265 -0
- package/dist/chunks/chunk-5QXGOLQ3.js +101 -0
- package/dist/chunks/chunk-62O2F73J.js +13 -0
- package/dist/chunks/chunk-CAQJPW54.js +211 -0
- package/dist/chunks/chunk-CUUNLVID.js +124 -0
- package/dist/chunks/chunk-EQPM4FIJ.js +419 -0
- package/dist/chunks/chunk-FVC2UPMV.js +306 -0
- package/dist/chunks/{chunk-3JNUWI22.js → chunk-NRXIFJGR.js} +3 -155
- package/dist/chunks/chunk-RZ4OBBLX.js +129 -0
- package/dist/chunks/chunk-UMPU6SAD.js +860 -0
- package/dist/chunks/{client-BKQLCKVT.js → client-UCMRJVMA.js} +1621 -437
- package/dist/chunks/livekit-client-bundle-HLYFRJB2.js +15 -0
- package/dist/chunks/livekit-terminal-22Q2NFB5.js +535 -0
- package/dist/chunks/livekit-window-XVOCBQOM.js +626 -0
- package/dist/chunks/server-A4T45QZ4.js +2707 -0
- package/dist/chunks/{update-UEPNGB6Y.js → update-RGDNBI4T.js} +25 -7
- package/dist/cli.js +132 -10
- package/package.json +10 -7
- package/dist/chunks/chunk-7JSMON7M.js +0 -64
- package/dist/chunks/chunk-HY6ULYGH.js +0 -559
- package/dist/chunks/server-IRLRECZ7.js +0 -1024
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/livekit-page.ts
|
|
4
|
+
function pinnedOrigins(liveKitUrl) {
|
|
5
|
+
const u = new URL(liveKitUrl);
|
|
6
|
+
const secure = u.protocol === "wss:" || u.protocol === "https:";
|
|
7
|
+
const host = u.host;
|
|
8
|
+
return [`${secure ? "wss" : "ws"}://${host}`, `${secure ? "https" : "http"}://${host}`];
|
|
9
|
+
}
|
|
10
|
+
function cloudFailoverOrigins(liveKitUrl) {
|
|
11
|
+
const host = new URL(liveKitUrl).hostname.toLowerCase();
|
|
12
|
+
for (const domain of ["livekit.cloud", "livekit.run"]) {
|
|
13
|
+
if (host.endsWith("." + domain)) return [`wss://*.${domain}`, `https://*.${domain}`];
|
|
14
|
+
}
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
function toWsUrl(url) {
|
|
18
|
+
if (url.startsWith("https://")) return "wss://" + url.slice(8);
|
|
19
|
+
if (url.startsWith("http://")) return "ws://" + url.slice(7);
|
|
20
|
+
return url;
|
|
21
|
+
}
|
|
22
|
+
var VOICE_PAGE_CSS = `
|
|
23
|
+
:root{color-scheme:light dark;--bg:#0d0f12;--fg:#e8e6e1;--dim:#8b8f96;--accent:#ff8a3d;--good:#4cc38a;--bad:#ff6b6b;--line:#262a31}
|
|
24
|
+
@media (prefers-color-scheme: light){:root{--bg:#f6f4ef;--fg:#16181c;--dim:#5f636a;--line:#d9d5cc}}
|
|
25
|
+
*{box-sizing:border-box}
|
|
26
|
+
html,body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
27
|
+
main{padding:20px 18px;max-width:440px}
|
|
28
|
+
h1{font-size:17px;margin:0 0 4px}
|
|
29
|
+
.room{color:var(--dim);font-size:12px;margin:0 0 16px}
|
|
30
|
+
#status{margin:0 0 16px;min-height:2.9em}
|
|
31
|
+
.row{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 16px}
|
|
32
|
+
button{font:inherit;padding:9px 14px;border-radius:6px;border:1px solid var(--line);background:transparent;color:var(--fg);cursor:pointer}
|
|
33
|
+
button.primary{background:var(--accent);border-color:var(--accent);color:#16181c;font-weight:700}
|
|
34
|
+
button.live{background:var(--good);border-color:var(--good);color:#16181c;font-weight:700}
|
|
35
|
+
button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
36
|
+
ul{list-style:none;margin:0;padding:0;border-top:1px solid var(--line)}
|
|
37
|
+
li{padding:7px 0;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:8px}
|
|
38
|
+
li .who{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
39
|
+
li .tag{color:var(--dim);font-size:12px;flex:none}
|
|
40
|
+
li.speaking .who{color:var(--good)}
|
|
41
|
+
.note{color:var(--dim);font-size:12px;margin-top:14px}
|
|
42
|
+
[hidden]{display:none!important}
|
|
43
|
+
`;
|
|
44
|
+
var VOICE_PAGE_JS = String.raw`(function () {
|
|
45
|
+
'use strict';
|
|
46
|
+
var boot = JSON.parse(document.getElementById('boot').textContent);
|
|
47
|
+
var LK = window.LivekitClient;
|
|
48
|
+
var base = '/voice/' + boot.nonce;
|
|
49
|
+
function $(id) { return document.getElementById(id); }
|
|
50
|
+
function text(id, s) { $(id).textContent = s; }
|
|
51
|
+
function show(id, on) { $(id).hidden = !on; }
|
|
52
|
+
|
|
53
|
+
var es = null, attach = null, bridgeTimer = null;
|
|
54
|
+
var room = null, micTrack = null, joining = false, joined = false;
|
|
55
|
+
var wantMuted = true, leaveAsked = false, ended = false, everConnected = false, failing = false;
|
|
56
|
+
var audioEls = {};
|
|
57
|
+
|
|
58
|
+
function post(path, body) {
|
|
59
|
+
var payload = { attach: attach };
|
|
60
|
+
for (var k in body) payload[k] = body[k];
|
|
61
|
+
return fetch(base + path, {
|
|
62
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify(payload), credentials: 'same-origin', cache: 'no-store',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function sendState(patch) {
|
|
67
|
+
if (!attach) return;
|
|
68
|
+
post('/state', patch).catch(function () { /* bridge gone: its own timer handles it */ });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------- the bridge ----------
|
|
72
|
+
function connectBridge() {
|
|
73
|
+
es = new EventSource(base + '/events');
|
|
74
|
+
es.addEventListener('hello', function (e) {
|
|
75
|
+
var d = {};
|
|
76
|
+
try { d = JSON.parse(e.data); } catch (err) { /* keep defaults */ }
|
|
77
|
+
attach = d.attach || null;
|
|
78
|
+
if (!joined && !joining && typeof d.muted === 'boolean') wantMuted = d.muted;
|
|
79
|
+
bridgeUp();
|
|
80
|
+
if (ended) return;
|
|
81
|
+
if (joined || joining) sendState(snapshot());
|
|
82
|
+
else ready();
|
|
83
|
+
});
|
|
84
|
+
es.addEventListener('cmd', function (e) {
|
|
85
|
+
var c = null;
|
|
86
|
+
try { c = JSON.parse(e.data); } catch (err) { return; }
|
|
87
|
+
if (c) command(c);
|
|
88
|
+
});
|
|
89
|
+
es.addEventListener('bye', function (e) {
|
|
90
|
+
var d = {};
|
|
91
|
+
try { d = JSON.parse(e.data); } catch (err) { /* no reason */ }
|
|
92
|
+
bridgeGone(d.reason || 'accelr8 ended voice');
|
|
93
|
+
});
|
|
94
|
+
es.onopen = bridgeUp;
|
|
95
|
+
es.onerror = function () {
|
|
96
|
+
// CLOSED: the server answered 404/409 (that session is over, or another
|
|
97
|
+
// window has it). Otherwise the browser is retrying: give it a moment.
|
|
98
|
+
if (es.readyState === 2) { bridgeGone('this voice window is closed'); return; }
|
|
99
|
+
if (!bridgeTimer) bridgeTimer = setTimeout(function () { bridgeGone('lost touch with accelr8'); }, boot.bridgeLossMs);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function bridgeUp() { if (bridgeTimer) { clearTimeout(bridgeTimer); bridgeTimer = null; } }
|
|
103
|
+
function bridgeGone(reason) {
|
|
104
|
+
if (ended) return;
|
|
105
|
+
bridgeUp();
|
|
106
|
+
try { es.close(); } catch (err) { /* already */ }
|
|
107
|
+
leaveAsked = true;
|
|
108
|
+
finish('Voice stopped: ' + reason + '. Run /voice in accelr8 to join again.', null);
|
|
109
|
+
if (room) room.disconnect();
|
|
110
|
+
stopMic();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function command(c) {
|
|
114
|
+
if (c.cmd === 'mute') { setMuted(!!c.muted); return; }
|
|
115
|
+
if (c.cmd === 'leave') { leave(); return; }
|
|
116
|
+
if (c.cmd === 'device' && typeof c.id === 'string') {
|
|
117
|
+
if (!room || !joined) return;
|
|
118
|
+
room.switchActiveDevice('audioinput', c.id).then(function () {
|
|
119
|
+
sendState({ inputDeviceId: c.id });
|
|
120
|
+
}, function (err) { sendState({ error: 'could not switch the mic: ' + msg(err) }); });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (c.cmd === 'devices') {
|
|
124
|
+
var kind = c.kind === 'audiooutput' ? 'audiooutput' : 'audioinput';
|
|
125
|
+
LK.Room.getLocalDevices(kind, false).then(function (list) {
|
|
126
|
+
post('/reply', { id: c.id, devices: list.map(function (d) {
|
|
127
|
+
return { id: d.deviceId, label: d.label || 'unnamed device', kind: d.kind, isDefault: d.deviceId === 'default' };
|
|
128
|
+
}) }).catch(function () {});
|
|
129
|
+
}, function () { post('/reply', { id: c.id, devices: [] }).catch(function () {}); });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------- page states ----------
|
|
134
|
+
function readyLine() {
|
|
135
|
+
// says what Join will really do: ctrl-g before the click opens the mic
|
|
136
|
+
return 'Click Join to start talking with the room. ' + (wantMuted ? 'You join muted.' : 'You join with your mic on.');
|
|
137
|
+
}
|
|
138
|
+
function ready() {
|
|
139
|
+
text('status', readyLine());
|
|
140
|
+
show('join', true); show('mute', false); show('leave', false);
|
|
141
|
+
$('join').textContent = 'Join voice';
|
|
142
|
+
sendState({ connection: 'connecting', needsClick: true, muted: true, participants: [] });
|
|
143
|
+
}
|
|
144
|
+
function finish(line, reason) {
|
|
145
|
+
if (ended) return;
|
|
146
|
+
ended = true;
|
|
147
|
+
text('status', line);
|
|
148
|
+
show('join', false); show('mute', false); show('leave', false);
|
|
149
|
+
$('people').textContent = '';
|
|
150
|
+
for (var k in audioEls) { try { audioEls[k].remove(); } catch (err) { /* gone */ } }
|
|
151
|
+
audioEls = {};
|
|
152
|
+
if (reason) sendState({ connection: 'disconnected', endReason: reason, needsClick: false });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function msg(err) { return (err && err.message) ? String(err.message).slice(0, 160) : String(err).slice(0, 160); }
|
|
156
|
+
|
|
157
|
+
// ---------- join, only from a click ----------
|
|
158
|
+
async function join() {
|
|
159
|
+
if (joining || joined || ended || !attach) return;
|
|
160
|
+
joining = true;
|
|
161
|
+
show('join', false);
|
|
162
|
+
text('status', 'Joining. Allow the microphone if Chrome asks.');
|
|
163
|
+
sendState({ connection: 'connecting', needsClick: false });
|
|
164
|
+
var token;
|
|
165
|
+
try {
|
|
166
|
+
var res = await post('/join', {});
|
|
167
|
+
if (!res.ok) throw new Error(res.status === 409 ? 'another voice window has this call' : 'accelr8 did not hand over a voice pass');
|
|
168
|
+
token = (await res.json()).token;
|
|
169
|
+
if (typeof token !== 'string' || !token) throw new Error('accelr8 did not hand over a voice pass');
|
|
170
|
+
} catch (err) { return joinFailed(err); }
|
|
171
|
+
if (leaveAsked || ended) { joining = false; return finish('You left voice. You can close this window.', 'left'); }
|
|
172
|
+
|
|
173
|
+
room = new LK.Room({
|
|
174
|
+
audioCaptureDefaults: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
|
|
175
|
+
adaptiveStream: false, dynacast: false, disconnectOnPageLeave: true,
|
|
176
|
+
});
|
|
177
|
+
wire(room);
|
|
178
|
+
try {
|
|
179
|
+
// boot.url is the pinned server; nothing from the network may change it
|
|
180
|
+
await room.connect(boot.url, token, { autoSubscribe: true });
|
|
181
|
+
everConnected = true;
|
|
182
|
+
if (leaveAsked || ended) { await room.disconnect(); return; }
|
|
183
|
+
var track = await LK.createLocalAudioTrack({ echoCancellation: true, noiseSuppression: true, autoGainControl: true });
|
|
184
|
+
micTrack = track;
|
|
185
|
+
if (leaveAsked || ended) { stopMic(); await room.disconnect(); return; }
|
|
186
|
+
if (wantMuted) await track.mute();
|
|
187
|
+
await room.localParticipant.publishTrack(track, { source: LK.Track.Source.Microphone, dtx: true, red: true });
|
|
188
|
+
if (leaveAsked || ended) { stopMic(); await room.disconnect(); return; }
|
|
189
|
+
// the user may have asked to talk (or to mute) while we were publishing
|
|
190
|
+
if (wantMuted !== track.isMuted) { if (wantMuted) await track.mute(); else await track.unmute(); }
|
|
191
|
+
try { await room.startAudio(); } catch (err) { /* playbackBlocked tells the terminal */ }
|
|
192
|
+
} catch (err) {
|
|
193
|
+
stopMic();
|
|
194
|
+
failing = true; // our own clean-up disconnect is not a 'left'
|
|
195
|
+
try { await room.disconnect(); } catch (e2) { /* already */ }
|
|
196
|
+
if (leaveAsked) { joining = false; return finish('You left voice. You can close this window.', 'left'); }
|
|
197
|
+
return joinFailed(err);
|
|
198
|
+
}
|
|
199
|
+
joining = false;
|
|
200
|
+
joined = true;
|
|
201
|
+
show('mute', true); show('leave', true);
|
|
202
|
+
render();
|
|
203
|
+
}
|
|
204
|
+
function joinFailed(err) {
|
|
205
|
+
if (ended) return;
|
|
206
|
+
joining = false;
|
|
207
|
+
finish('Could not join voice: ' + msg(err) + '. Close this window and run /voice in accelr8 again.', null);
|
|
208
|
+
// drop the bridge only once the terminal has the reason: closing it first
|
|
209
|
+
// raced the POST, and the terminal then saw a closed window, not the error
|
|
210
|
+
var drop = function () { try { es.close(); } catch (e2) { /* already */ } };
|
|
211
|
+
if (!attach) { drop(); return; }
|
|
212
|
+
post('/state', { connection: 'disconnected', endReason: 'error', error: msg(err), needsClick: false }).then(drop, drop);
|
|
213
|
+
}
|
|
214
|
+
function stopMic() {
|
|
215
|
+
if (micTrack) { try { micTrack.stop(); } catch (err) { /* already */ } }
|
|
216
|
+
micTrack = null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function leave() {
|
|
220
|
+
leaveAsked = true;
|
|
221
|
+
if (room && (joined || joining)) { room.disconnect(); return; } // Disconnected reports 'left'
|
|
222
|
+
if (!joining) finish('You left voice. You can close this window.', 'left');
|
|
223
|
+
// joining: each await in join() checks leaveAsked and hangs up
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function setMuted(m) {
|
|
227
|
+
wantMuted = m;
|
|
228
|
+
if (!micTrack || !joined) {
|
|
229
|
+
// still waiting for the click: keep the line true to what Join will do
|
|
230
|
+
if (attach && !joining && !joined && !ended) text('status', readyLine());
|
|
231
|
+
sendState({ muted: true });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
var p = m ? micTrack.mute() : micTrack.unmute();
|
|
235
|
+
Promise.resolve(p).then(render, function (err) { sendState({ error: 'could not change the mic: ' + msg(err) }); });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------- room events ----------
|
|
239
|
+
function wire(r) {
|
|
240
|
+
var E = LK.RoomEvent;
|
|
241
|
+
r.on(E.TrackSubscribed, function (track, pub, participant) {
|
|
242
|
+
if (track.kind !== 'audio') return;
|
|
243
|
+
var el = track.attach();
|
|
244
|
+
el.hidden = true;
|
|
245
|
+
document.body.appendChild(el);
|
|
246
|
+
audioEls[pub.trackSid] = el;
|
|
247
|
+
render();
|
|
248
|
+
});
|
|
249
|
+
r.on(E.TrackUnsubscribed, function (track, pub) {
|
|
250
|
+
var els = track.detach() || [];
|
|
251
|
+
for (var i = 0; i < els.length; i++) els[i].remove();
|
|
252
|
+
delete audioEls[pub.trackSid];
|
|
253
|
+
render();
|
|
254
|
+
});
|
|
255
|
+
[E.ParticipantConnected, E.ParticipantDisconnected, E.TrackMuted, E.TrackUnmuted, E.ActiveSpeakersChanged,
|
|
256
|
+
E.ParticipantMetadataChanged, E.ParticipantNameChanged, E.LocalTrackPublished, E.ConnectionStateChanged,
|
|
257
|
+
E.AudioPlaybackStatusChanged].forEach(function (ev) { if (ev) r.on(ev, render); });
|
|
258
|
+
r.on(E.Disconnected, function (reason) {
|
|
259
|
+
stopMic();
|
|
260
|
+
if (failing && !leaveAsked) return; // our own clean-up after a failed join
|
|
261
|
+
if (joining && !leaveAsked) {
|
|
262
|
+
// livekit-client says Disconnected a moment BEFORE connect() rejects
|
|
263
|
+
// with the real message; let join() report that, with a backstop in
|
|
264
|
+
// case the join is stuck somewhere that never settles
|
|
265
|
+
setTimeout(function () { joinFailed(new Error('the voice server closed the connection')); }, 1500);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
var code = typeof reason === 'number' ? reason : undefined;
|
|
269
|
+
var why = leaveAsked ? 'left' : mapReason(code);
|
|
270
|
+
finish(why === 'left' ? 'You left voice. You can close this window.' : 'Voice ended (' + why.replace('_', ' ') + '). Run /voice in accelr8 to join again.', why);
|
|
271
|
+
if (why === 'left') { try { window.close(); } catch (err) { /* not allowed: the line above says so */ } }
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function mapReason(code) {
|
|
275
|
+
if (code === undefined && everConnected) return 'network';
|
|
276
|
+
switch (code) {
|
|
277
|
+
case 1: return 'left';
|
|
278
|
+
case 2: return 'replaced';
|
|
279
|
+
case 4: return 'removed';
|
|
280
|
+
case 5: case 10: return 'room_closed';
|
|
281
|
+
case 3: case 7: case 9: case 14: case 15: return 'network';
|
|
282
|
+
default: return 'error';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function meta(p) {
|
|
287
|
+
try { var m = JSON.parse(p.metadata || ''); return (m && m.v === 1) ? m : {}; } catch (err) { return {}; }
|
|
288
|
+
}
|
|
289
|
+
function person(p, isLocal) {
|
|
290
|
+
var m = meta(p);
|
|
291
|
+
var muted = isLocal ? (!micTrack || micTrack.isMuted) : !p.isMicrophoneEnabled;
|
|
292
|
+
return {
|
|
293
|
+
identity: p.identity, sid: m.sid || null, userId: m.userId || null,
|
|
294
|
+
name: String(m.name || p.name || 'guest'), color: typeof m.color === 'number' ? m.color : null,
|
|
295
|
+
client: m.client === 'web' ? 'web' : 'tui', isLocal: isLocal, muted: muted, speaking: !muted && !!p.isSpeaking,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function snapshot() {
|
|
299
|
+
var list = [];
|
|
300
|
+
if (room && joined) {
|
|
301
|
+
list.push(person(room.localParticipant, true));
|
|
302
|
+
room.remoteParticipants.forEach(function (p) { list.push(person(p, false)); });
|
|
303
|
+
}
|
|
304
|
+
var state = room ? room.state : '';
|
|
305
|
+
return {
|
|
306
|
+
connection: !joined ? 'connecting' : state === 'reconnecting' || state === 'signalReconnecting' ? 'reconnecting' : 'connected',
|
|
307
|
+
needsClick: false,
|
|
308
|
+
muted: !micTrack || micTrack.isMuted,
|
|
309
|
+
playbackBlocked: !!room && !room.canPlaybackAudio,
|
|
310
|
+
participants: list,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function render() {
|
|
314
|
+
if (ended || !joined) return;
|
|
315
|
+
var s = snapshot();
|
|
316
|
+
var ul = $('people');
|
|
317
|
+
ul.textContent = '';
|
|
318
|
+
s.participants.forEach(function (p) {
|
|
319
|
+
var li = document.createElement('li');
|
|
320
|
+
if (p.speaking) li.className = 'speaking';
|
|
321
|
+
var who = document.createElement('span'); who.className = 'who';
|
|
322
|
+
who.textContent = p.name + (p.isLocal ? ' (you)' : '');
|
|
323
|
+
var tag = document.createElement('span'); tag.className = 'tag';
|
|
324
|
+
tag.textContent = p.speaking ? 'talking' : p.muted ? 'muted' : 'mic on';
|
|
325
|
+
li.appendChild(who); li.appendChild(tag); ul.appendChild(li);
|
|
326
|
+
});
|
|
327
|
+
var m = $('mute');
|
|
328
|
+
m.textContent = s.muted ? 'Unmute (M)' : 'Mute (M)';
|
|
329
|
+
m.className = s.muted ? '' : 'live';
|
|
330
|
+
if (s.playbackBlocked) {
|
|
331
|
+
text('status', 'Chrome is holding back the sound. Click Turn on sound.');
|
|
332
|
+
show('join', true); $('join').textContent = 'Turn on sound';
|
|
333
|
+
} else {
|
|
334
|
+
text('status', s.connection === 'reconnecting' ? 'Reconnecting.' : s.muted ? 'In voice, muted.' : 'In voice. Your mic is on.');
|
|
335
|
+
show('join', false);
|
|
336
|
+
}
|
|
337
|
+
sendState(s);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------- controls ----------
|
|
341
|
+
$('join').addEventListener('click', function () {
|
|
342
|
+
if (joined && room) { room.startAudio().then(render, render); return; }
|
|
343
|
+
join();
|
|
344
|
+
});
|
|
345
|
+
$('mute').addEventListener('click', function () { setMuted(micTrack ? !micTrack.isMuted : true); });
|
|
346
|
+
$('leave').addEventListener('click', leave);
|
|
347
|
+
document.addEventListener('keydown', function (e) {
|
|
348
|
+
if (e.repeat || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
349
|
+
var t = e.target;
|
|
350
|
+
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName || ''))) return;
|
|
351
|
+
if ((e.key === 'm' || e.key === 'M') && joined && micTrack) { e.preventDefault(); setMuted(!micTrack.isMuted); }
|
|
352
|
+
});
|
|
353
|
+
window.addEventListener('pagehide', function () {
|
|
354
|
+
if (attach && navigator.sendBeacon) navigator.sendBeacon(base + '/closing', JSON.stringify({ attach: attach }));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
connectBridge();
|
|
358
|
+
})();
|
|
359
|
+
`;
|
|
360
|
+
function escapeHtml(s) {
|
|
361
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
362
|
+
}
|
|
363
|
+
function scriptJson(v) {
|
|
364
|
+
return JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
365
|
+
}
|
|
366
|
+
function voicePageHtml(o) {
|
|
367
|
+
const boot = { nonce: o.nonce, url: toWsUrl(o.liveKitUrl), bridgeLossMs: o.bridgeLossMs ?? 4e3 };
|
|
368
|
+
const title = escapeHtml(o.title || "accelr8");
|
|
369
|
+
return `<!doctype html>
|
|
370
|
+
<html lang="en">
|
|
371
|
+
<head>
|
|
372
|
+
<meta charset="utf-8">
|
|
373
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
374
|
+
<meta name="referrer" content="no-referrer">
|
|
375
|
+
<link rel="icon" href="data:,">
|
|
376
|
+
<title>${title} \xB7 accelr8 voice</title>
|
|
377
|
+
<style>${VOICE_PAGE_CSS}</style>
|
|
378
|
+
</head>
|
|
379
|
+
<body>
|
|
380
|
+
<main>
|
|
381
|
+
<h1>${title}</h1>
|
|
382
|
+
<p class="room">accelr8 voice</p>
|
|
383
|
+
<p id="status">Connecting to accelr8.</p>
|
|
384
|
+
<div class="row">
|
|
385
|
+
<button id="join" class="primary" hidden>Join voice</button>
|
|
386
|
+
<button id="mute" hidden>Unmute (M)</button>
|
|
387
|
+
<button id="leave" hidden>Leave voice</button>
|
|
388
|
+
</div>
|
|
389
|
+
<ul id="people"></ul>
|
|
390
|
+
<p class="note">Your terminal controls this window too: ctrl-g talks or mutes, /voice leaves.</p>
|
|
391
|
+
</main>
|
|
392
|
+
<script type="application/json" id="boot">${scriptJson(boot)}</script>
|
|
393
|
+
<script src="/voice/${o.nonce}/livekit-client.js"></script>
|
|
394
|
+
<script src="/voice/${o.nonce}/app.js"></script>
|
|
395
|
+
</body>
|
|
396
|
+
</html>
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
399
|
+
function voicePageCsp(liveKitUrl, styleHash) {
|
|
400
|
+
return [
|
|
401
|
+
"default-src 'none'",
|
|
402
|
+
"script-src 'self'",
|
|
403
|
+
`style-src 'sha256-${styleHash}'`,
|
|
404
|
+
`connect-src 'self' ${[...pinnedOrigins(liveKitUrl), ...cloudFailoverOrigins(liveKitUrl)].join(" ")}`,
|
|
405
|
+
"media-src 'self' blob: mediastream:",
|
|
406
|
+
"img-src 'self' data:",
|
|
407
|
+
"base-uri 'none'",
|
|
408
|
+
"form-action 'none'",
|
|
409
|
+
"frame-ancestors 'none'"
|
|
410
|
+
].join("; ");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export {
|
|
414
|
+
toWsUrl,
|
|
415
|
+
VOICE_PAGE_CSS,
|
|
416
|
+
VOICE_PAGE_JS,
|
|
417
|
+
voicePageHtml,
|
|
418
|
+
voicePageCsp
|
|
419
|
+
};
|