@rapidmx/meet-plugin 0.5.0 → 0.8.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/README.md +15 -2
- package/apps/meet/[token].tsx +3 -2
- package/apps/meet/_CallControls.tsx +28 -2
- package/apps/meet/_CallView.tsx +23 -2
- package/apps/meet/_EffectsPanel.tsx +134 -0
- package/apps/meet/_MeetLobby.tsx +22 -0
- package/apps/meet/_ParticipantTile.tsx +62 -9
- package/apps/meet/_icons.tsx +4 -0
- package/apps/meet/_meetApi.ts +6 -0
- package/apps/shared/media/filters/VideoFilterProcessor.ts +383 -0
- package/apps/shared/media/filters/accessories.ts +262 -0
- package/apps/shared/media/filters/backgroundImage.ts +69 -0
- package/apps/shared/media/filters/filterTypes.ts +89 -0
- package/apps/shared/media/filters/mlModels.ts +174 -0
- package/apps/shared/media/filters/pixelEffects.ts +61 -0
- package/apps/shared/media/mediaPreferences.ts +105 -0
- package/apps/shared/media/useLocalMedia.ts +198 -14
- package/apps/shared/relay/AudioPlayer.ts +111 -0
- package/apps/shared/relay/AudioSender.ts +194 -0
- package/apps/shared/relay/RelayClient.ts +262 -0
- package/apps/shared/relay/RelayReceiver.ts +72 -0
- package/apps/shared/relay/RelaySender.ts +74 -0
- package/apps/shared/relay/RelayTransport.ts +188 -0
- package/apps/shared/relay/VideoPlayer.ts +110 -0
- package/apps/shared/relay/VideoSender.ts +216 -0
- package/apps/shared/relay/audioResume.ts +55 -0
- package/apps/shared/relay/frames.ts +275 -0
- package/apps/shared/relay/relayEnv.ts +230 -0
- package/apps/shared/relay/senderCommon.ts +30 -0
- package/apps/shared/webrtc/MeshConnectionManager.ts +158 -6
- package/apps/shared/webrtc/realPeerConnection.ts +22 -0
- package/apps/shared/webrtc/types.ts +32 -2
- package/dist/apps/meet/[token].js +3 -3
- package/dist/apps/meet/_CallControls.d.ts +2 -1
- package/dist/apps/meet/_CallControls.js +6 -3
- package/dist/apps/meet/_CallView.d.ts +4 -1
- package/dist/apps/meet/_CallView.js +17 -3
- package/dist/apps/meet/_EffectsPanel.d.ts +17 -0
- package/dist/apps/meet/_EffectsPanel.js +60 -0
- package/dist/apps/meet/_MeetLobby.d.ts +4 -0
- package/dist/apps/meet/_MeetLobby.js +8 -1
- package/dist/apps/meet/_ParticipantTile.d.ts +7 -1
- package/dist/apps/meet/_ParticipantTile.js +20 -3
- package/dist/apps/meet/_icons.d.ts +1 -0
- package/dist/apps/meet/_icons.js +1 -0
- package/dist/apps/meet/_meetApi.d.ts +6 -0
- package/dist/apps/shared/media/filters/VideoFilterProcessor.d.ts +75 -0
- package/dist/apps/shared/media/filters/VideoFilterProcessor.js +304 -0
- package/dist/apps/shared/media/filters/accessories.d.ts +37 -0
- package/dist/apps/shared/media/filters/accessories.js +211 -0
- package/dist/apps/shared/media/filters/backgroundImage.d.ts +22 -0
- package/dist/apps/shared/media/filters/backgroundImage.js +54 -0
- package/dist/apps/shared/media/filters/filterTypes.d.ts +40 -0
- package/dist/apps/shared/media/filters/filterTypes.js +63 -0
- package/dist/apps/shared/media/filters/mlModels.d.ts +54 -0
- package/dist/apps/shared/media/filters/mlModels.js +106 -0
- package/dist/apps/shared/media/filters/pixelEffects.d.ts +14 -0
- package/dist/apps/shared/media/filters/pixelEffects.js +55 -0
- package/dist/apps/shared/media/mediaPreferences.d.ts +34 -0
- package/dist/apps/shared/media/mediaPreferences.js +93 -0
- package/dist/apps/shared/media/useLocalMedia.d.ts +17 -1
- package/dist/apps/shared/media/useLocalMedia.js +160 -14
- package/dist/apps/shared/relay/AudioPlayer.d.ts +38 -0
- package/dist/apps/shared/relay/AudioPlayer.js +103 -0
- package/dist/apps/shared/relay/AudioSender.d.ts +50 -0
- package/dist/apps/shared/relay/AudioSender.js +172 -0
- package/dist/apps/shared/relay/RelayClient.d.ts +65 -0
- package/dist/apps/shared/relay/RelayClient.js +236 -0
- package/dist/apps/shared/relay/RelayReceiver.d.ts +21 -0
- package/dist/apps/shared/relay/RelayReceiver.js +59 -0
- package/dist/apps/shared/relay/RelaySender.d.ts +34 -0
- package/dist/apps/shared/relay/RelaySender.js +54 -0
- package/dist/apps/shared/relay/RelayTransport.d.ts +38 -0
- package/dist/apps/shared/relay/RelayTransport.js +142 -0
- package/dist/apps/shared/relay/VideoPlayer.d.ts +31 -0
- package/dist/apps/shared/relay/VideoPlayer.js +102 -0
- package/dist/apps/shared/relay/VideoSender.d.ts +56 -0
- package/dist/apps/shared/relay/VideoSender.js +204 -0
- package/dist/apps/shared/relay/audioResume.d.ts +12 -0
- package/dist/apps/shared/relay/audioResume.js +45 -0
- package/dist/apps/shared/relay/frames.d.ts +101 -0
- package/dist/apps/shared/relay/frames.js +221 -0
- package/dist/apps/shared/relay/relayEnv.d.ts +181 -0
- package/dist/apps/shared/relay/relayEnv.js +69 -0
- package/dist/apps/shared/relay/senderCommon.d.ts +19 -0
- package/dist/apps/shared/relay/senderCommon.js +10 -0
- package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +51 -1
- package/dist/apps/shared/webrtc/MeshConnectionManager.js +137 -6
- package/dist/apps/shared/webrtc/realPeerConnection.d.ts +7 -0
- package/dist/apps/shared/webrtc/realPeerConnection.js +20 -0
- package/dist/apps/shared/webrtc/types.d.ts +29 -2
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/routes/BaseVideoMeetingRoute.js +237 -3
- package/dist/lib/routes/BaseVideoMeetingRoute.js.map +1 -1
- package/dist/lib/util/EffectsUtils.js +21 -0
- package/dist/lib/util/EffectsUtils.js.map +1 -0
- package/dist/lib/util/IceServerUtils.js +25 -4
- package/dist/lib/util/IceServerUtils.js.map +1 -1
- package/dist/lib/util/RedisRelayBus.js +241 -0
- package/dist/lib/util/RedisRelayBus.js.map +1 -0
- package/dist/lib/util/RelayHub.js +405 -0
- package/dist/lib/util/RelayHub.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/routes/BaseVideoMeetingRoute.d.ts +79 -0
- package/dist/types/util/EffectsUtils.d.ts +10 -0
- package/dist/types/util/IceServerUtils.d.ts +12 -1
- package/dist/types/util/RedisRelayBus.d.ts +131 -0
- package/dist/types/util/RelayHub.d.ts +188 -0
- package/package.json +24 -7
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/** The version of the relay wire protocol this module implements - the `v` of `hello`/`ready`. */
|
|
6
|
+
export const RELAY_PROTOCOL_VERSION = 1;
|
|
7
|
+
/** The largest binary payload (in bytes, excluding the sender header the server adds) a socket may send; anything
|
|
8
|
+
* larger or empty is dropped. Matches `@rapidrest/service-core`'s uWS `DEFAULT_WS_OPTIONS.maxPayloadLength` (16 KiB),
|
|
9
|
+
* which this framework's route registration cannot raise - a larger message closes the socket at the transport. */
|
|
10
|
+
export const RELAY_MAX_PAYLOAD_BYTES = 16 * 1024;
|
|
11
|
+
/** The larger message limit the relay route asks the framework for when the installed `@rapidrest/service-core` lets a
|
|
12
|
+
* `@WebSocket()` route set one (its 2.4.0 and later): a video key frame then fits in one message instead of several,
|
|
13
|
+
* with far less fragmenting and reassembly for the client to get wrong. The hub advertises whichever limit applies to
|
|
14
|
+
* the client in `ready` (`maxMessageBytes`). */
|
|
15
|
+
export const RELAY_LARGE_PAYLOAD_BYTES = 64 * 1024;
|
|
16
|
+
/** How many bytes the framework may hold unsent for one relay socket before it drops further messages to it, when the
|
|
17
|
+
* installed `@rapidrest/service-core` lets a route set that (its default, 64 KiB, is smaller than one key frame's worth
|
|
18
|
+
* of headroom and made a briefly slow receiver lose frames constantly). One MiB is roughly two seconds of a 350 kbps
|
|
19
|
+
* video stream plus audio. */
|
|
20
|
+
export const RELAY_WS_MAX_BACKPRESSURE_BYTES = 1024 * 1024;
|
|
21
|
+
/** The most sockets (registered or still awaiting their `hello`) one room holds at once. */
|
|
22
|
+
export const RELAY_MAX_SOCKETS_PER_ROOM = 16;
|
|
23
|
+
/** The most sockets one authenticated uid may hold in one room at once (several tabs or devices of one account). */
|
|
24
|
+
export const RELAY_MAX_SOCKETS_PER_UID = 4;
|
|
25
|
+
/** Sustained inbound binary budget per socket, in bytes per second (a token bucket refill rate). */
|
|
26
|
+
export const RELAY_RATE_BYTES_PER_SECOND = 512 * 1024;
|
|
27
|
+
/** Burst allowance of a socket's token bucket, in bytes - also the bucket's capacity. */
|
|
28
|
+
export const RELAY_BURST_BYTES = 1024 * 1024;
|
|
29
|
+
/** How long a socket may stay over budget continuously before it is closed with 1008 "Too much data.". */
|
|
30
|
+
export const RELAY_OVER_BUDGET_CLOSE_MS = 10000;
|
|
31
|
+
/** Two over-budget drops further apart than this are separate episodes: a socket is "continuously" over budget only
|
|
32
|
+
* while it keeps being dropped at least this often. A sender that exceeds its budget by a hair (one drop every few
|
|
33
|
+
* seconds) is throttled but never closed; one that keeps sending well past it is closed after
|
|
34
|
+
* `RELAY_OVER_BUDGET_CLOSE_MS`. */
|
|
35
|
+
export const RELAY_OVER_BUDGET_GRACE_MS = 1000;
|
|
36
|
+
/** How long a new socket has to send its `hello` before it is closed with 1008 "Hello timeout.". */
|
|
37
|
+
export const RELAY_HELLO_TIMEOUT_MS = 10000;
|
|
38
|
+
/** The longest peer id, in characters. Its UTF-8 encoding must also fit in the one length byte of a forwarded frame
|
|
39
|
+
* (255), which `isValidRelayPeer()` enforces too. */
|
|
40
|
+
export const RELAY_MAX_PEER_LENGTH = 128;
|
|
41
|
+
/** The most peers one `want` message may name; further valid entries are ignored. */
|
|
42
|
+
export const RELAY_MAX_WANT_PEERS = 32;
|
|
43
|
+
/** A `RelayBus` that delivers synchronously to the listeners of this one process - see `RelayHub`'s doc comment for
|
|
44
|
+
* what that means for a deployment with several server replicas. */
|
|
45
|
+
export class InProcessRelayBus {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.listeners = new Map();
|
|
48
|
+
}
|
|
49
|
+
subscribe(roomId, listener) {
|
|
50
|
+
let set = this.listeners.get(roomId);
|
|
51
|
+
if (!set) {
|
|
52
|
+
set = new Set();
|
|
53
|
+
this.listeners.set(roomId, set);
|
|
54
|
+
}
|
|
55
|
+
set.add(listener);
|
|
56
|
+
return () => {
|
|
57
|
+
set.delete(listener);
|
|
58
|
+
if (set.size === 0 && this.listeners.get(roomId) === set) {
|
|
59
|
+
this.listeners.delete(roomId);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
publish(roomId, sender, frame) {
|
|
64
|
+
for (const listener of this.listeners.get(roomId) ?? []) {
|
|
65
|
+
listener(sender, frame);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Returns whether `peer` is a well-formed relay peer id: a string of 1 to `RELAY_MAX_PEER_LENGTH` characters with no
|
|
71
|
+
* whitespace, separator, control or other invisible (Unicode category C or Z) character, whose UTF-8 encoding fits in
|
|
72
|
+
* the single length byte of a forwarded frame.
|
|
73
|
+
*/
|
|
74
|
+
export function isValidRelayPeer(peer) {
|
|
75
|
+
return (typeof peer === "string" &&
|
|
76
|
+
peer.length >= 1 &&
|
|
77
|
+
peer.length <= RELAY_MAX_PEER_LENGTH &&
|
|
78
|
+
/^[^\s\p{C}\p{Z}]+$/u.test(peer) &&
|
|
79
|
+
Buffer.byteLength(peer, "utf8") <= 255);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Reads the `mail:videoconf:relay:enabled` setting. Environment-variable config arrives as a string ("true", "false",
|
|
83
|
+
* "0", "1") and nconf may parse it into a boolean or a number, so all of those are accepted; `false`, `0`, `"false"`
|
|
84
|
+
* and `"0"` (case-insensitive, trimmed) disable the relay and anything else - including an unset or unrecognizable
|
|
85
|
+
* value - leaves it enabled, the default.
|
|
86
|
+
*/
|
|
87
|
+
export function parseRelayEnabled(value) {
|
|
88
|
+
if (typeof value === "string") {
|
|
89
|
+
const normalized = value.trim().toLowerCase();
|
|
90
|
+
return normalized !== "false" && normalized !== "0";
|
|
91
|
+
}
|
|
92
|
+
return value !== false && value !== 0;
|
|
93
|
+
}
|
|
94
|
+
function toBuffer(data) {
|
|
95
|
+
if (typeof data === "string") {
|
|
96
|
+
return Buffer.from(data, "utf8");
|
|
97
|
+
}
|
|
98
|
+
return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
99
|
+
}
|
|
100
|
+
/** A token bucket: `capacity` tokens (bytes), refilled at `rate` per second, starting full. */
|
|
101
|
+
class TokenBucket {
|
|
102
|
+
constructor(now) {
|
|
103
|
+
this.tokens = RELAY_BURST_BYTES;
|
|
104
|
+
this.last = now;
|
|
105
|
+
}
|
|
106
|
+
/** Takes `amount` tokens if that many are available; returns whether it did. */
|
|
107
|
+
take(amount, now) {
|
|
108
|
+
this.tokens = Math.min(RELAY_BURST_BYTES, this.tokens + ((now - this.last) / 1000) * RELAY_RATE_BYTES_PER_SECOND);
|
|
109
|
+
this.last = now;
|
|
110
|
+
if (this.tokens < amount) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
this.tokens -= amount;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
class Connection {
|
|
118
|
+
constructor(env, room, uid, socket) {
|
|
119
|
+
this.env = env;
|
|
120
|
+
this.room = room;
|
|
121
|
+
this.uid = uid;
|
|
122
|
+
this.socket = socket;
|
|
123
|
+
/** The peer ids this socket wants frames from - see `want`. */
|
|
124
|
+
this.wanted = new Set();
|
|
125
|
+
this.closed = false;
|
|
126
|
+
this.bucket = new TokenBucket(Date.now());
|
|
127
|
+
this.lastOverBudget = 0;
|
|
128
|
+
this.helloTimer = setTimeout(() => this.evict("Hello timeout."), RELAY_HELLO_TIMEOUT_MS);
|
|
129
|
+
this.helloTimer.unref();
|
|
130
|
+
}
|
|
131
|
+
message(data, isBinary) {
|
|
132
|
+
if (this.closed) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (isBinary) {
|
|
136
|
+
this.binary(toBuffer(data));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.text(typeof data === "string" ? data : toBuffer(data).toString("utf8"));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
close() {
|
|
143
|
+
if (this.closed) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.closed = true;
|
|
147
|
+
clearTimeout(this.helloTimer);
|
|
148
|
+
this.env.remove(this);
|
|
149
|
+
}
|
|
150
|
+
/** Closes the socket from this side with 1008 and `reason`, and forgets it. Only ever called on a live
|
|
151
|
+
* connection: `message()` ignores a closed one, its hello timer is cleared on close, and a closed one is no
|
|
152
|
+
* longer in its room's `peers`. */
|
|
153
|
+
evict(reason) {
|
|
154
|
+
this.close();
|
|
155
|
+
this.socket.close(1008, reason);
|
|
156
|
+
}
|
|
157
|
+
/** Sends `frame` to this socket. A send the socket reports as dropped, or throws on, is counted and skipped. */
|
|
158
|
+
forward(frame) {
|
|
159
|
+
const fail = (err) => {
|
|
160
|
+
this.env.stats.framesDroppedBackpressure++;
|
|
161
|
+
this.env.debug(`Dropped a relay frame for ${this.peer} in ${this.room.id}: ${err?.message ?? err}`);
|
|
162
|
+
};
|
|
163
|
+
try {
|
|
164
|
+
this.socket.send(frame, (err) => {
|
|
165
|
+
if (err) {
|
|
166
|
+
fail(err);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
this.env.stats.framesForwarded++;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
fail(err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Sends `text` to this socket, ignoring a failure - a lost `ready` shows as a client that never gets ready. */
|
|
176
|
+
sendText(text) {
|
|
177
|
+
try {
|
|
178
|
+
this.socket.send(text);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// The socket is going away; its close event cleans up.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
text(text) {
|
|
185
|
+
// A character is at least one byte, so this bounds the parse below by the payload limit.
|
|
186
|
+
if (text.length > this.env.maxPayloadBytes) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let message;
|
|
190
|
+
try {
|
|
191
|
+
message = JSON.parse(text);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!this.peer) {
|
|
200
|
+
if (message.op === "hello") {
|
|
201
|
+
this.hello(message);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (message.op === "want") {
|
|
205
|
+
this.want(message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
hello(message) {
|
|
209
|
+
if (message.v !== RELAY_PROTOCOL_VERSION) {
|
|
210
|
+
this.evict("Unsupported version.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const peer = message.peer;
|
|
214
|
+
if (!isValidRelayPeer(peer) || (peer !== this.uid && !peer.startsWith(`${this.uid}~`))) {
|
|
215
|
+
this.evict("Invalid peer.");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// A reconnecting client's new socket replaces the old one, which may not have noticed it is dead yet.
|
|
219
|
+
this.room.peers.get(peer)?.evict("Replaced.");
|
|
220
|
+
this.room.peers.set(peer, this);
|
|
221
|
+
this.peer = peer;
|
|
222
|
+
this.peerBytes = Buffer.from(peer, "utf8");
|
|
223
|
+
clearTimeout(this.helloTimer);
|
|
224
|
+
this.sendText(JSON.stringify({ op: "ready", v: RELAY_PROTOCOL_VERSION, maxMessageBytes: this.env.maxPayloadBytes }));
|
|
225
|
+
}
|
|
226
|
+
want(message) {
|
|
227
|
+
if (!Array.isArray(message.peers)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const wanted = new Set();
|
|
231
|
+
for (const peer of message.peers) {
|
|
232
|
+
if (wanted.size >= RELAY_MAX_WANT_PEERS) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (isValidRelayPeer(peer)) {
|
|
236
|
+
wanted.add(peer);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
this.wanted = wanted;
|
|
240
|
+
}
|
|
241
|
+
binary(payload) {
|
|
242
|
+
if (!this.peer || payload.length === 0 || payload.length > this.env.maxPayloadBytes) {
|
|
243
|
+
this.env.stats.framesDroppedInvalid++;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const now = Date.now();
|
|
247
|
+
if (!this.bucket.take(payload.length, now)) {
|
|
248
|
+
this.env.stats.framesDroppedBudget++;
|
|
249
|
+
if (this.overBudgetSince === undefined || now - this.lastOverBudget > RELAY_OVER_BUDGET_GRACE_MS) {
|
|
250
|
+
this.overBudgetSince = now;
|
|
251
|
+
}
|
|
252
|
+
this.lastOverBudget = now;
|
|
253
|
+
if (now - this.overBudgetSince > RELAY_OVER_BUDGET_CLOSE_MS) {
|
|
254
|
+
this.evict("Too much data.");
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Stamped here from the `hello`, never read from the payload: [sender length][sender][payload].
|
|
259
|
+
const sender = this.peerBytes;
|
|
260
|
+
const frame = Buffer.allocUnsafe(1 + sender.length + payload.length);
|
|
261
|
+
frame[0] = sender.length;
|
|
262
|
+
sender.copy(frame, 1);
|
|
263
|
+
payload.copy(frame, 1 + sender.length);
|
|
264
|
+
this.env.bus.publish(this.room.id, this.peer, frame);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* A fan-out relay of opaque binary frames between the participants of a room (one meeting) - the last-resort tier
|
|
269
|
+
* for participants whose network allows neither a direct WebRTC connection nor a TURN relay, only a WebSocket to this
|
|
270
|
+
* server. It knows nothing about meetings, users, HTTP or any framework: a caller authorizes a socket however it
|
|
271
|
+
* likes, hands it to `attach()` and feeds the returned `RelayConnection` the socket's `message` and `close` events.
|
|
272
|
+
*
|
|
273
|
+
* ## Wire protocol (version 1)
|
|
274
|
+
*
|
|
275
|
+
* Client to server, the first message must be a text message
|
|
276
|
+
* `{"op":"hello","v":1,"peer":"<peerId>"}`, sent within `RELAY_HELLO_TIMEOUT_MS`. `peer` is the caller's uid (the `uid`
|
|
277
|
+
* given to `attach()`) or starts with `<uid>~` - a per-tab or per-device id - and passes `isValidRelayPeer()`. Anything
|
|
278
|
+
* else closes the socket with 1008 "Invalid peer."; a wrong `v` closes it with 1008 "Unsupported version.". The server
|
|
279
|
+
* answers with the text message `{"op":"ready","v":1,"maxMessageBytes":<the limit below>}`. A second socket registering an already registered peer in the
|
|
280
|
+
* same room replaces the older one, which is closed with 1008 "Replaced.".
|
|
281
|
+
*
|
|
282
|
+
* After that, `{"op":"want","peers":["<peerId>", ...]}` REPLACES the socket's interest set with the valid entries, at
|
|
283
|
+
* most `RELAY_MAX_WANT_PEERS` of them; every other or malformed text message is ignored. Binary messages of 1 to
|
|
284
|
+
* `RelayHubOptions.maxPayloadBytes` bytes (`RELAY_MAX_PAYLOAD_BYTES` unless the route asked for more; opaque to the hub) are forwarded to every other socket of the room whose interest
|
|
285
|
+
* set contains the sender's peer id; binary before `hello`, empty and oversized messages are dropped.
|
|
286
|
+
*
|
|
287
|
+
* Server to client: `{"op":"ready","v":1,"maxMessageBytes":N}` once, then binary messages `[1 byte: UTF-8 length N of the sender's peer
|
|
288
|
+
* id][N bytes: the sender's peer id][the original payload]`. The sender is stamped by the hub from the `hello`,
|
|
289
|
+
* never trusted from the payload.
|
|
290
|
+
*
|
|
291
|
+
* ## Limits
|
|
292
|
+
*
|
|
293
|
+
* At most `RELAY_MAX_SOCKETS_PER_ROOM` sockets per room and `RELAY_MAX_SOCKETS_PER_UID` per uid per room, counting
|
|
294
|
+
* a socket from `attach()` on (a socket refused for either is closed with 1008 "Room full." / "Too many
|
|
295
|
+
* connections."). Each socket's inbound binary traffic is metered by a token bucket of `RELAY_RATE_BYTES_PER_SECOND`
|
|
296
|
+
* sustained with a `RELAY_BURST_BYTES` burst: an over-budget message is dropped silently, and a socket kept over
|
|
297
|
+
* budget for more than `RELAY_OVER_BUDGET_CLOSE_MS` (see `RELAY_OVER_BUDGET_GRACE_MS`) is closed with 1008 "Too
|
|
298
|
+
* much data.". A receiver whose `send()` reports the message as dropped (uWS backpressure) simply misses that
|
|
299
|
+
* frame - counted in `stats`, never queued or retried. Sockets and rooms are released when their socket closes; an
|
|
300
|
+
* empty room is deleted.
|
|
301
|
+
*
|
|
302
|
+
* ## Replicas
|
|
303
|
+
*
|
|
304
|
+
* The transport between sockets is the `RelayBus` given to the constructor, `InProcessRelayBus` by default, which
|
|
305
|
+
* delivers only within this process: **with several server replicas, participants whose sockets landed on different
|
|
306
|
+
* replicas cannot relay to each other** - a room exists independently on each replica and its members see only the
|
|
307
|
+
* members connected to the same one. Give the hub a `RedisRelayBus` and frames cross replicas over Redis pub/sub (one
|
|
308
|
+
* channel per room; `BaseVideoMeetingRoute` does that whenever `datastores:events` is configured); nothing else in the
|
|
309
|
+
* hub changes for it.
|
|
310
|
+
*
|
|
311
|
+
* Every limit above is enforced **per replica**, because a hub only knows its own sockets. A room's capacity across
|
|
312
|
+
* replicas is therefore up to `replicas x RELAY_MAX_SOCKETS_PER_ROOM` sockets (and `replicas x
|
|
313
|
+
* RELAY_MAX_SOCKETS_PER_UID` for one uid) in the worst case, where the load balancer spreads a meeting's sockets
|
|
314
|
+
* evenly. A peer id that registers on two replicas is not detected as a duplicate either (the "Replaced." rule is
|
|
315
|
+
* per replica), and each frame reaches every socket that wants its sender on every replica.
|
|
316
|
+
*
|
|
317
|
+
* @author Jean-Philippe Steinmetz
|
|
318
|
+
*/
|
|
319
|
+
export class RelayHub {
|
|
320
|
+
constructor(options = {}) {
|
|
321
|
+
/** What has happened to frames so far. */
|
|
322
|
+
this.stats = {
|
|
323
|
+
framesForwarded: 0,
|
|
324
|
+
framesDroppedBackpressure: 0,
|
|
325
|
+
framesDroppedBudget: 0,
|
|
326
|
+
framesDroppedInvalid: 0,
|
|
327
|
+
};
|
|
328
|
+
this.rooms = new Map();
|
|
329
|
+
this.bus = options.bus ?? new InProcessRelayBus();
|
|
330
|
+
this.logger = options.logger;
|
|
331
|
+
this.env = {
|
|
332
|
+
stats: this.stats,
|
|
333
|
+
bus: this.bus,
|
|
334
|
+
maxPayloadBytes: options.maxPayloadBytes ?? RELAY_MAX_PAYLOAD_BYTES,
|
|
335
|
+
debug: (message) => this.logger?.debug(message),
|
|
336
|
+
remove: (conn) => this.remove(conn),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/** The number of rooms that currently hold at least one socket. */
|
|
340
|
+
get roomCount() {
|
|
341
|
+
return this.rooms.size;
|
|
342
|
+
}
|
|
343
|
+
/** The number of sockets in `roomId` (0 for an unknown room). */
|
|
344
|
+
socketCount(roomId) {
|
|
345
|
+
return this.rooms.get(roomId)?.conns.size ?? 0;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Accepts an already-authorized `socket` of `uid` into room `roomId`, or refuses it (closing it with 1008) when
|
|
349
|
+
* the room or the uid is at its limit. Returns the connection to feed the socket's events to, or `undefined`
|
|
350
|
+
* when refused.
|
|
351
|
+
*/
|
|
352
|
+
attach(roomId, uid, socket) {
|
|
353
|
+
let room = this.rooms.get(roomId);
|
|
354
|
+
if (room) {
|
|
355
|
+
if (room.conns.size >= RELAY_MAX_SOCKETS_PER_ROOM) {
|
|
356
|
+
socket.close(1008, "Room full.");
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
let ofUid = 0;
|
|
360
|
+
for (const conn of room.conns) {
|
|
361
|
+
if (conn.uid === uid) {
|
|
362
|
+
ofUid++;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (ofUid >= RELAY_MAX_SOCKETS_PER_UID) {
|
|
366
|
+
socket.close(1008, "Too many connections.");
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
room = {
|
|
372
|
+
id: roomId,
|
|
373
|
+
conns: new Set(),
|
|
374
|
+
peers: new Map(),
|
|
375
|
+
unsubscribe: this.bus.subscribe(roomId, (sender, frame) => this.deliver(roomId, sender, frame)),
|
|
376
|
+
};
|
|
377
|
+
this.rooms.set(roomId, room);
|
|
378
|
+
}
|
|
379
|
+
const conn = new Connection(this.env, room, uid, socket);
|
|
380
|
+
room.conns.add(conn);
|
|
381
|
+
return conn;
|
|
382
|
+
}
|
|
383
|
+
/** Forgets `conn`, and its room once that is empty. */
|
|
384
|
+
remove(conn) {
|
|
385
|
+
const room = conn.room;
|
|
386
|
+
room.conns.delete(conn);
|
|
387
|
+
if (conn.peer && room.peers.get(conn.peer) === conn) {
|
|
388
|
+
room.peers.delete(conn.peer);
|
|
389
|
+
}
|
|
390
|
+
if (room.conns.size === 0 && this.rooms.get(room.id) === room) {
|
|
391
|
+
room.unsubscribe();
|
|
392
|
+
this.rooms.delete(room.id);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** Hands `frame` from `sender` to every other socket of room `roomId` that wants it. A bus that delivers
|
|
396
|
+
* asynchronously (across replicas) may deliver a frame for a room that has just emptied; that is ignored. */
|
|
397
|
+
deliver(roomId, sender, frame) {
|
|
398
|
+
for (const conn of this.rooms.get(roomId)?.peers.values() ?? []) {
|
|
399
|
+
if (conn.peer !== sender && conn.wanted.has(sender)) {
|
|
400
|
+
conn.forward(frame);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
//# sourceMappingURL=RelayHub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RelayHub.js","sourceRoot":"","sources":["../../../src/util/RelayHub.ts"],"names":[],"mappings":"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"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export * from "./models/types.js";
|
|
|
15
15
|
export * from "./util/BookingIntegrationUtils.js";
|
|
16
16
|
export * from "./util/IceServerUtils.js";
|
|
17
17
|
export * from "./util/PublicUrlUtils.js";
|
|
18
|
+
export * from "./util/RedisRelayBus.js";
|
|
19
|
+
export * from "./util/RelayHub.js";
|
|
18
20
|
export * from "./util/RouteAccessUtils.js";
|
|
19
21
|
export * from "./util/TokenUtils.js";
|
|
20
22
|
export * from "./routes/BaseVideoMeetingRoute.js";
|
|
@@ -98,6 +98,13 @@ export interface VideoMeetingJoinResult {
|
|
|
98
98
|
/** ISO 8601 instant `token` expires at. Present only when `authenticated` is `false`, exactly when `token`
|
|
99
99
|
* itself is. */
|
|
100
100
|
expiresAt?: string;
|
|
101
|
+
/** Whether the last-resort WebSocket media relay (`relay()`, the third tier after a direct WebRTC connection and
|
|
102
|
+
* TURN) is enabled on this server (`mail:videoconf:relay:enabled`). A client only falls back to it when this is
|
|
103
|
+
* `true`; connecting to a disabled relay is refused anyway. */
|
|
104
|
+
relayEnabled: boolean;
|
|
105
|
+
/** Where the video filters' machine-learning runtime and models are hosted, when the administrator hosts them
|
|
106
|
+
* (`mail:videoconf:effects:assets_url`). Omitted when unset, in which case clients use the public CDNs. */
|
|
107
|
+
effectsAssetsUrl?: string;
|
|
101
108
|
}
|
|
102
109
|
/**
|
|
103
110
|
* The owner's management of their own `VideoMeeting`s (JWT-authenticated, mailbox-ACL-checked exactly like any
|
|
@@ -222,6 +229,27 @@ export interface VideoMeetingJoinResult {
|
|
|
222
229
|
* now run every loaded meeting through `withJoinUrls()` - the exact same computation `create()` already did,
|
|
223
230
|
* applied uniformly on read instead of only once on write.
|
|
224
231
|
*
|
|
232
|
+
* ## The last-resort WebSocket media relay (`relay()`)
|
|
233
|
+
*
|
|
234
|
+
* Clients connect peer-to-peer over WebRTC, fall back to TURN, and as a third and last resort (a corporate firewall
|
|
235
|
+
* that allows neither) send media over a WebSocket this server proxies: `relay()`, at `/relay/:id` under the
|
|
236
|
+
* subclass's own base path (`/api/mail/video-meetings/relay/<meetingUid>`). It is a fan-out of opaque binary
|
|
237
|
+
* frames between the participants of one meeting - the wire protocol and limits are documented on `RelayHub`, which
|
|
238
|
+
* does all of the relaying; this class only authorizes a socket and hands it over, and picks how frames travel
|
|
239
|
+
* between server replicas: when the deployment configures `datastores:events` (the Redis the framework's push system
|
|
240
|
+
* already uses) frames cross replicas over Redis pub/sub (`RedisRelayBus`), so participants connected to different
|
|
241
|
+
* replicas reach each other; without it they travel in-process only, with a one-time warning at start-up that relay
|
|
242
|
+
* does not cross replicas.
|
|
243
|
+
*
|
|
244
|
+
* Authorization is the very grant `join()`'s `ensureChannelGrant()` already gives every participant, guest or real:
|
|
245
|
+
* `READ` and `CREATE` on the meeting's uid, checked with the caller's trusted roles stripped exactly as
|
|
246
|
+
* `MailPushRoute` does for `/push`, so a trusted administrator with no grant is refused just like there. The meeting must exist and be neither
|
|
247
|
+
* cancelled nor ended. Every refusal closes the socket with code 1008; a missing meeting and a missing grant are
|
|
248
|
+
* indistinguishable ("Not permitted."), so a caller learns nothing about meetings they can't join. The whole
|
|
249
|
+
* thing can be turned off with `mail:videoconf:relay:enabled` (default `true`; `false`/`"false"`/`0`/`"0"` disable
|
|
250
|
+
* it): a disabled relay closes every socket with 1008 "Relay disabled." and `join()` reports `relayEnabled: false`, so
|
|
251
|
+
* clients don't try it.
|
|
252
|
+
*
|
|
225
253
|
* ## Other known limitations
|
|
226
254
|
*
|
|
227
255
|
* **`VideoMeetingInvitee.joinToken` never expires** and has no GC job - identical tradeoff to `Booking.manageToken`.
|
|
@@ -261,7 +289,22 @@ export declare abstract class BaseVideoMeetingRoute<VM extends VideoMeeting, VMI
|
|
|
261
289
|
private turnUsername;
|
|
262
290
|
private turnCredential;
|
|
263
291
|
private turnSharedSecret;
|
|
292
|
+
/** Whether `relay()` accepts sockets, and `join()` advertises it - read through `isRelayEnabled()`, since a value
|
|
293
|
+
* set from an environment variable arrives as a string. */
|
|
294
|
+
private relayEnabledSetting;
|
|
295
|
+
/** The base URL the video filters' models are hosted under, when not on the public CDNs - read through
|
|
296
|
+
* `parseEffectsAssetsUrl()`, which drops anything that isn't a plain URL or path. */
|
|
297
|
+
private effectsAssetsUrlSetting;
|
|
298
|
+
/** The deployment's `events` datastore (`datastores:events`) - the Redis the framework's push system publishes
|
|
299
|
+
* across replicas on; `null` when the deployment has none. Decides how `initRelayBus()` carries relay frames. */
|
|
300
|
+
private eventsConfig;
|
|
264
301
|
private logger;
|
|
302
|
+
/** The rooms of this process's relay sockets. One per route instance. It starts out on an in-process bus, which
|
|
303
|
+
* is what a route that was never initialized (a test's) keeps; `initRelayBus()` swaps in a hub on a Redis bus
|
|
304
|
+
* when the deployment has one, before any socket can attach. Its socket caps are per server replica. */
|
|
305
|
+
private relayHub;
|
|
306
|
+
/** The Redis bus `relayHub` runs on, when there is one. */
|
|
307
|
+
private relayBus?;
|
|
265
308
|
/**
|
|
266
309
|
* Exposes the `@Model(...)`-supplied entity class as an instance property so `@Transactional()` on
|
|
267
310
|
* `persistMeeting()` can resolve which datasource to open a transaction against - identical to
|
|
@@ -269,6 +312,17 @@ export declare abstract class BaseVideoMeetingRoute<VM extends VideoMeeting, VMI
|
|
|
269
312
|
* `ModelRoute`, which defines the same getter for its own subclasses).
|
|
270
313
|
*/
|
|
271
314
|
get modelClass(): any;
|
|
315
|
+
/**
|
|
316
|
+
* Picks how relay frames travel between server replicas, once the route is created: over Redis pub/sub
|
|
317
|
+
* (`RedisRelayBus`) when `datastores:events` is configured, else in-process only with a single warning that
|
|
318
|
+
* participants on different replicas cannot relay to each other. A Redis bus that cannot be created (the `redis`
|
|
319
|
+
* package is missing, an unusable URL) falls back the same way, with the error logged. Runs before the route
|
|
320
|
+
* serves anything, so no socket is attached to the hub it replaces.
|
|
321
|
+
*/
|
|
322
|
+
initRelayBus(): Promise<void>;
|
|
323
|
+
/** Disconnects the Redis relay bus, if there is one, when the object factory destroys the route. (The factory
|
|
324
|
+
* runs one destroy method per object: a subclass declaring its own `@Destroy` must call this one.) */
|
|
325
|
+
destroyRelayBus(): void;
|
|
272
326
|
private init;
|
|
273
327
|
/** Rejects a `403` unless `user` (with its trusted roles stripped - see `util/RouteAccessUtils.ts`) holds
|
|
274
328
|
* `action` on `mailboxUid`, by ownership or an explicit ACL grant. */
|
|
@@ -380,4 +434,29 @@ export declare abstract class BaseVideoMeetingRoute<VM extends VideoMeeting, VMI
|
|
|
380
434
|
* orphaned meeting should still be joinable, just without a host name to show). */
|
|
381
435
|
private hostDisplayName;
|
|
382
436
|
join(token: string, user?: JWTUser): Promise<VideoMeetingJoinResult>;
|
|
437
|
+
/** Whether the WebSocket media relay is enabled - `mail:videoconf:relay:enabled`, see `parseRelayEnabled()`. */
|
|
438
|
+
private isRelayEnabled;
|
|
439
|
+
/**
|
|
440
|
+
* Decides whether `user` may open a relay socket to meeting `id`: the relay must be enabled, `user` must hold BOTH
|
|
441
|
+
* `READ` and `CREATE` on the meeting's uid - exactly what `ensureChannelGrant()` gives a joined participant, and
|
|
442
|
+
* what `/push` demands to subscribe and publish - and the meeting must exist and be neither cancelled nor ended.
|
|
443
|
+
* Returns the reason to close the socket with, or `undefined` when permitted.
|
|
444
|
+
*
|
|
445
|
+
* `hasPermission()` gets `user` with its trusted roles stripped (`stripTrustedRoles()`), exactly as
|
|
446
|
+
* `@rapidmx/restapi`'s `MailPushRoute` does for the same grant on `/push`: without it the framework's "trusted
|
|
447
|
+
* users always have permission" rule would let any administrator relay in any meeting they hold no grant on, so
|
|
448
|
+
* a trusted administrator with no grant is refused here exactly as `/push` refuses them. Every authorization
|
|
449
|
+
* failure gives the same `RELAY_NOT_PERMITTED`, whether the grant, the meeting or its state is what's missing.
|
|
450
|
+
*/
|
|
451
|
+
private authorizeRelay;
|
|
452
|
+
/**
|
|
453
|
+
* The meeting uid a relay socket's URL names. `@Param("id")` is filled in by the Bun router, but NOT by the uWS
|
|
454
|
+
* router this framework actually serves with: it never populates `req.params` for a WebSocket upgrade (a
|
|
455
|
+
* `@WebSocket()` route has, so far, never had a path parameter - `BasePushRoute.connect()` has none). So when the
|
|
456
|
+
* decorated argument is empty this reads the last segment of the request path instead, which the
|
|
457
|
+
* `/relay/:id` pattern guarantees is the id (the framework also registers the trailing-slash form; the path
|
|
458
|
+
* never carries the query string). Returns `undefined` when there is none or it isn't valid percent-encoding.
|
|
459
|
+
*/
|
|
460
|
+
private relayMeetingId;
|
|
461
|
+
relay(paramId: string | undefined, req: HttpRequest, sock: any, user?: JWTUser): Promise<void>;
|
|
383
462
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the `mail:videoconf:effects:assets_url` setting: the base URL an administrator hosts the video filters'
|
|
3
|
+
* machine-learning runtime and models under, in place of the public CDNs they are otherwise fetched from. It is
|
|
4
|
+
* handed to every participant's browser, so only an absolute `http(s)` URL or a path on this server is accepted;
|
|
5
|
+
* anything else (a `javascript:` URL, a typo, nothing at all) is treated as unset. A trailing slash is dropped.
|
|
6
|
+
*
|
|
7
|
+
* @param value The setting as configured.
|
|
8
|
+
* @returns The URL to hand to clients, or `undefined` to have them use the defaults.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseEffectsAssetsUrl(value: unknown): string | undefined;
|
|
@@ -17,6 +17,17 @@ export interface TurnSettings {
|
|
|
17
17
|
/** The URLs in a `mail:videoconf:turn:url` value: one, or several separated by commas and/or whitespace (for a TURN server
|
|
18
18
|
* listening on both UDP and TLS, `turn:host:3478,turns:host:5349`). Blank entries are dropped. */
|
|
19
19
|
export declare function parseTurnUrls(value: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Adds the TCP flavour of every plain `turn:` URL that does not name a transport itself. A browser given
|
|
22
|
+
* `turn:host:3478` speaks only UDP to the TURN server, and UDP is precisely what a restrictive firewall blocks while
|
|
23
|
+
* still letting outbound TCP through - so a client behind one never reaches a TURN server that was listening on TCP
|
|
24
|
+
* all along. Offering both lets ICE try UDP first and settle for TCP when UDP goes nowhere. `turns:` URLs are TCP
|
|
25
|
+
* (TLS) already, and a URL with an explicit `transport=` is the operator's own choice and is left exactly as given.
|
|
26
|
+
*
|
|
27
|
+
* @param urls The URLs from `parseTurnUrls()`, in the order the operator wrote them.
|
|
28
|
+
* @returns The same URLs, each plain `turn:` one followed by its `?transport=tcp` twin, without duplicates.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withTcpFallback(urls: string[]): string[];
|
|
20
31
|
/** Always-available, free public STUN servers - discovery only, no media relay. Two independent providers, so a
|
|
21
32
|
* call still has STUN available if one of them is ever unreachable. */
|
|
22
33
|
export declare const DEFAULT_STUN_SERVERS: readonly IceServerConfig[];
|
|
@@ -44,7 +55,7 @@ export declare function turnRestCredential(sharedSecret: string, userPart: strin
|
|
|
44
55
|
};
|
|
45
56
|
/**
|
|
46
57
|
* Builds the full ICE server list for a join response: the hardcoded public STUN servers, plus a TURN entry when
|
|
47
|
-
* `settings.url` is configured. When a `sharedSecret` is also configured, the TURN entry carries a fresh,
|
|
58
|
+
* `settings.url` is configured (see `withTcpFallback()` for the URLs it carries). When a `sharedSecret` is also configured, the TURN entry carries a fresh,
|
|
48
59
|
* time-limited credential (`turnRestCredential()`) rather than the static `username`/`credential` pair, which is
|
|
49
60
|
* used verbatim only when no shared secret is set. A TURN url with neither a shared secret nor a static
|
|
50
61
|
* credential is still included with no credentials at all - some self-hosted TURN servers are deliberately run
|