@rapidmx/meet-plugin 0.9.3 → 0.10.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/apps/meet/[token].tsx +204 -13
- package/apps/meet/_CallControls.tsx +64 -3
- package/apps/meet/_CallView.tsx +244 -7
- package/apps/meet/_DiagnosticsPanel.tsx +113 -0
- package/apps/meet/_ParticipantTile.tsx +10 -2
- package/apps/meet/_ParticipantsDrawer.tsx +341 -0
- package/apps/meet/_icons.tsx +12 -0
- package/apps/meet/_meetApi.ts +141 -1
- package/apps/shared/media/filters/ScreenTransform.ts +148 -0
- package/apps/shared/media/useLocalMedia.ts +7 -3
- package/apps/shared/webrtc/MeshConnectionManager.ts +89 -4
- package/apps/shared/webrtc/realPeerConnection.ts +47 -7
- package/apps/shared/webrtc/types.ts +55 -7
- package/dist/apps/meet/[token].d.ts +4 -1
- package/dist/apps/meet/[token].js +121 -9
- package/dist/apps/meet/_CallControls.d.ts +9 -1
- package/dist/apps/meet/_CallControls.js +6 -5
- package/dist/apps/meet/_CallView.d.ts +28 -2
- package/dist/apps/meet/_CallView.js +155 -6
- package/dist/apps/meet/_DiagnosticsPanel.d.ts +22 -0
- package/dist/apps/meet/_DiagnosticsPanel.js +32 -0
- package/dist/apps/meet/_ParticipantTile.d.ts +8 -0
- package/dist/apps/meet/_ParticipantTile.js +9 -2
- package/dist/apps/meet/_ParticipantsDrawer.d.ts +61 -0
- package/dist/apps/meet/_ParticipantsDrawer.js +86 -0
- package/dist/apps/meet/_icons.d.ts +3 -0
- package/dist/apps/meet/_icons.js +3 -0
- package/dist/apps/meet/_meetApi.d.ts +93 -1
- package/dist/apps/meet/_meetApi.js +82 -0
- package/dist/apps/shared/media/filters/ScreenTransform.d.ts +53 -0
- package/dist/apps/shared/media/filters/ScreenTransform.js +100 -0
- package/dist/apps/shared/media/useLocalMedia.d.ts +5 -1
- package/dist/apps/shared/media/useLocalMedia.js +3 -3
- package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +30 -1
- package/dist/apps/shared/webrtc/MeshConnectionManager.js +78 -3
- package/dist/apps/shared/webrtc/realPeerConnection.d.ts +19 -6
- package/dist/apps/shared/webrtc/realPeerConnection.js +42 -5
- package/dist/apps/shared/webrtc/types.d.ts +54 -6
- package/dist/lib/models/mongo/VideoMeetingMongo.js +21 -0
- package/dist/lib/models/mongo/VideoMeetingMongo.js.map +1 -1
- package/dist/lib/models/sql/VideoMeetingSQL.js +21 -0
- package/dist/lib/models/sql/VideoMeetingSQL.js.map +1 -1
- package/dist/lib/routes/BaseVideoMeetingRoute.js +380 -51
- package/dist/lib/routes/BaseVideoMeetingRoute.js.map +1 -1
- package/dist/lib/util/PasswordUtils.js +52 -0
- package/dist/lib/util/PasswordUtils.js.map +1 -0
- package/dist/types/models/mongo/VideoMeetingMongo.d.ts +3 -0
- package/dist/types/models/sql/VideoMeetingSQL.d.ts +3 -0
- package/dist/types/models/types.d.ts +23 -0
- package/dist/types/routes/BaseVideoMeetingRoute.d.ts +147 -5
- package/dist/types/util/PasswordUtils.d.ts +17 -0
- package/package.json +1 -1
|
@@ -62,6 +62,15 @@
|
|
|
62
62
|
*
|
|
63
63
|
* Sharing a screen is `setLocalTrack("video", screenTrack)`; stopping is `setLocalTrack("video", cameraTrack)`.
|
|
64
64
|
*
|
|
65
|
+
* ## Host moderation (`mute-request`, `kicked`)
|
|
66
|
+
*
|
|
67
|
+
* `sendMuteRequest()`/`sendKick()` send a point-to-point signal to one peer; the manager itself does not decide who
|
|
68
|
+
* may send one - a "host" identity exists only in `_CallView.tsx`, which shows the controls that call these
|
|
69
|
+
* methods to nobody but the host (see `types.ts`'s `SignalMessage` doc comment for why this is cooperative, not
|
|
70
|
+
* enforced, at the signaling layer). Receiving one emits `mute-requested`/`kicked` for the caller to act on; this
|
|
71
|
+
* manager takes no action of its own beyond that - it doesn't mute a track or tear itself down, since a kicked
|
|
72
|
+
* participant's actual departure is `stop()`, which the caller decides to call.
|
|
73
|
+
*
|
|
65
74
|
* ## Three media paths, tried in order
|
|
66
75
|
*
|
|
67
76
|
* Each pair of participants reaches for the best path that works, and `MeshParticipant.transport` says which one it
|
|
@@ -98,6 +107,10 @@ const MAX_ORPHAN_CANDIDATES = 64;
|
|
|
98
107
|
export const DEFAULT_CONNECT_TIMEOUT_MS = 15000;
|
|
99
108
|
/** How long a connection may stay `disconnected` (ICE lost the path but may still recover) before giving up on it. */
|
|
100
109
|
export const DEFAULT_DISCONNECTED_GRACE_MS = 8000;
|
|
110
|
+
/** How often a connected pair's quality stats (the diagnostics panel) are refreshed - low frequency on purpose:
|
|
111
|
+
* this is for a participant occasionally checking why a call feels off, not a live chart, and `getStats()` is not
|
|
112
|
+
* free to call every frame. */
|
|
113
|
+
export const DEFAULT_DIAGNOSTICS_POLL_MS = 3000;
|
|
101
114
|
export class MeshConnectionManager {
|
|
102
115
|
constructor(options) {
|
|
103
116
|
this.options = options;
|
|
@@ -152,6 +165,7 @@ export class MeshConnectionManager {
|
|
|
152
165
|
this.unsubscribe = undefined;
|
|
153
166
|
for (const peer of this.peers.values()) {
|
|
154
167
|
this.clearTimers(peer);
|
|
168
|
+
this.stopDiagnosticsPolling(peer);
|
|
155
169
|
peer.pc.close();
|
|
156
170
|
}
|
|
157
171
|
this.peers.clear();
|
|
@@ -209,6 +223,18 @@ export class MeshConnectionManager {
|
|
|
209
223
|
this.send({ kind: "presenter-release" });
|
|
210
224
|
this.emit({ type: "presenter-changed", uid: undefined });
|
|
211
225
|
}
|
|
226
|
+
/** Asks `peerUid`'s participant to mute - a cooperative signal (see this module's doc comment on host
|
|
227
|
+
* moderation), not enforcement. Nothing here checks that the local participant is actually the host; the caller
|
|
228
|
+
* is responsible for only offering this to one. */
|
|
229
|
+
sendMuteRequest(peerUid) {
|
|
230
|
+
this.send({ kind: "mute-request", to: peerUid });
|
|
231
|
+
}
|
|
232
|
+
/** Tells `peerUid`'s participant they have been removed from the call. Sends only the cooperative signal - it
|
|
233
|
+
* does not itself revoke `peerUid`'s server-side channel grant (see `BaseVideoMeetingRoute.revokeChannelGrant()`,
|
|
234
|
+
* which the caller is expected to call alongside this for an enforced removal). */
|
|
235
|
+
sendKick(peerUid) {
|
|
236
|
+
this.send({ kind: "kicked", to: peerUid });
|
|
237
|
+
}
|
|
212
238
|
sendHello() {
|
|
213
239
|
this.send({ kind: "hello", name: this.options.selfName, state: this.localState });
|
|
214
240
|
}
|
|
@@ -266,6 +292,12 @@ export class MeshConnectionManager {
|
|
|
266
292
|
case "relay-fallback":
|
|
267
293
|
this.handleRelayFallback(message.from);
|
|
268
294
|
return;
|
|
295
|
+
case "mute-request":
|
|
296
|
+
this.emit({ type: "mute-requested" });
|
|
297
|
+
return;
|
|
298
|
+
case "kicked":
|
|
299
|
+
this.emit({ type: "kicked" });
|
|
300
|
+
return;
|
|
269
301
|
}
|
|
270
302
|
}
|
|
271
303
|
handleHello(message) {
|
|
@@ -324,6 +356,7 @@ export class MeshConnectionManager {
|
|
|
324
356
|
return;
|
|
325
357
|
}
|
|
326
358
|
this.clearTimers(peer);
|
|
359
|
+
this.stopDiagnosticsPolling(peer);
|
|
327
360
|
peer.pc.close();
|
|
328
361
|
this.peers.delete(uid);
|
|
329
362
|
if (peer.transport === "websocket") {
|
|
@@ -355,6 +388,7 @@ export class MeshConnectionManager {
|
|
|
355
388
|
transport: "connecting",
|
|
356
389
|
connectTimer: undefined,
|
|
357
390
|
disconnectTimer: undefined,
|
|
391
|
+
diagnosticsTimer: undefined,
|
|
358
392
|
};
|
|
359
393
|
this.orphanCandidates.delete(uid);
|
|
360
394
|
this.peers.set(uid, peer);
|
|
@@ -397,7 +431,8 @@ export class MeshConnectionManager {
|
|
|
397
431
|
return;
|
|
398
432
|
}
|
|
399
433
|
}
|
|
400
|
-
/** Records whether a connected pair is direct
|
|
434
|
+
/** Records whether a connected pair is direct, relayed through TURN, or relayed through TURN over TCP (see
|
|
435
|
+
* `MediaTransport`'s doc comment), from the candidate pair ICE selected. */
|
|
401
436
|
async detectTransport(peer) {
|
|
402
437
|
let type;
|
|
403
438
|
try {
|
|
@@ -408,7 +443,37 @@ export class MeshConnectionManager {
|
|
|
408
443
|
}
|
|
409
444
|
// The pair may have moved on (fallen back, left, or dropped again) while the stats were being read.
|
|
410
445
|
if (peer.transport !== "websocket" && this.peers.get(peer.uid) === peer && peer.pc.connectionState === "connected") {
|
|
411
|
-
this.setTransport(peer, type === "turn"
|
|
446
|
+
this.setTransport(peer, type === "turn" || type === "turn-tcp" ? type : "p2p");
|
|
447
|
+
this.startDiagnosticsPolling(peer);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Idempotent - a pair that reconnects without ever leaving WebRTC (`connected` -> `disconnected` -> `connected`
|
|
451
|
+
* again) does not get a second timer. */
|
|
452
|
+
startDiagnosticsPolling(peer) {
|
|
453
|
+
if (peer.diagnosticsTimer !== undefined) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const pollMs = this.options.diagnosticsPollMs ?? DEFAULT_DIAGNOSTICS_POLL_MS;
|
|
457
|
+
peer.diagnosticsTimer = setInterval(() => void this.pollDiagnostics(peer), pollMs);
|
|
458
|
+
void this.pollDiagnostics(peer); // the first sample doesn't wait a full interval
|
|
459
|
+
}
|
|
460
|
+
stopDiagnosticsPolling(peer) {
|
|
461
|
+
clearInterval(peer.diagnosticsTimer);
|
|
462
|
+
peer.diagnosticsTimer = undefined;
|
|
463
|
+
}
|
|
464
|
+
async pollDiagnostics(peer) {
|
|
465
|
+
let diagnostics;
|
|
466
|
+
try {
|
|
467
|
+
diagnostics = await peer.pc.collectDiagnostics();
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Leaves whatever was last polled in place rather than blanking a momentary failure.
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// The pair may have moved on (fallen back, left, or dropped again) while the stats were being read.
|
|
474
|
+
if (this.peers.get(peer.uid) === peer && peer.diagnosticsTimer !== undefined) {
|
|
475
|
+
peer.diagnostics = diagnostics;
|
|
476
|
+
this.emit({ type: "participant-updated", participant: toParticipant(peer) });
|
|
412
477
|
}
|
|
413
478
|
}
|
|
414
479
|
setTransport(peer, transport) {
|
|
@@ -433,6 +498,8 @@ export class MeshConnectionManager {
|
|
|
433
498
|
return;
|
|
434
499
|
}
|
|
435
500
|
this.clearTimers(peer);
|
|
501
|
+
this.stopDiagnosticsPolling(peer);
|
|
502
|
+
peer.diagnostics = undefined;
|
|
436
503
|
const relay = this.options.relay;
|
|
437
504
|
if (!relay?.supported) {
|
|
438
505
|
this.setTransport(peer, "failed");
|
|
@@ -569,7 +636,15 @@ export class MeshConnectionManager {
|
|
|
569
636
|
}
|
|
570
637
|
}
|
|
571
638
|
function toParticipant(peer) {
|
|
572
|
-
return {
|
|
639
|
+
return {
|
|
640
|
+
uid: peer.uid,
|
|
641
|
+
name: peer.name,
|
|
642
|
+
audioOn: peer.audioOn,
|
|
643
|
+
videoOn: peer.videoOn,
|
|
644
|
+
handRaised: peer.handRaised,
|
|
645
|
+
transport: peer.transport,
|
|
646
|
+
diagnostics: peer.diagnostics,
|
|
647
|
+
};
|
|
573
648
|
}
|
|
574
649
|
function sameState(a, b) {
|
|
575
650
|
return a.audioOn === b.audioOn && a.videoOn === b.videoOn && a.handRaised === b.handRaised;
|
|
@@ -3,12 +3,25 @@
|
|
|
3
3
|
* this factory's only real caller. `createBrowserPeerConnection` is a plain function value, never invoked at
|
|
4
4
|
* module scope, so importing this file is safe during this plugin's page SSR, where `RTCPeerConnection` does not
|
|
5
5
|
* exist at all - it would only throw if actually *called* outside a browser. */
|
|
6
|
-
import type { RTCPeerConnectionFactory } from "./types.js";
|
|
6
|
+
import type { ConnectionDiagnostics, RTCPeerConnectionFactory } from "./types.js";
|
|
7
7
|
/**
|
|
8
|
-
* Whether the pair ICE settled on goes through a TURN relay
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Whether the pair ICE settled on goes through a TURN relay, and - when it's our own end that's relayed - whether
|
|
9
|
+
* that relay is TCP (`turn-tcp`, meaning `turns:` too, TLS being TCP-based as well) rather than UDP. The selected
|
|
10
|
+
* pair is found the standard way (the `transport` report's `selectedCandidatePairId`) and, for a browser that does
|
|
11
|
+
* not fill that in, as the succeeded and nominated `candidate-pair`. A relay on *either* end means the media
|
|
12
|
+
* passes through a TURN server, so it counts as at least `"turn"`; `"unknown"` when no pair is reported at all.
|
|
13
|
+
*
|
|
14
|
+
* The TCP/UDP distinction matters because they fail very differently under loss: UDP just drops the lost packet
|
|
15
|
+
* (a click), while TCP retransmits it and blocks everything queued behind it until that arrives (a stall) - exactly
|
|
16
|
+
* the "gaps that feel like latency, not packet loss" a participant on a TCP-relayed connection reports. `relayProtocol`
|
|
17
|
+
* (`"udp"`/`"tcp"`/`"tls"`) is only ever reported on a *local* relay candidate - there's no equivalent visibility into
|
|
18
|
+
* how the *remote* peer reaches its own TURN allocation, so a relay on the remote end alone is reported as plain
|
|
19
|
+
* `"turn"` rather than guessed at.
|
|
12
20
|
*/
|
|
13
|
-
export declare function selectedConnectionType(pc: Pick<RTCPeerConnection, "getStats">): Promise<"p2p" | "turn" | "unknown">;
|
|
21
|
+
export declare function selectedConnectionType(pc: Pick<RTCPeerConnection, "getStats">): Promise<"p2p" | "turn" | "turn-tcp" | "unknown">;
|
|
22
|
+
/** One point-in-time sample of this connection's own quality stats, for the diagnostics panel - a single
|
|
23
|
+
* `getStats()` walk, independent of (and in addition to) `selectedConnectionType()`'s own call, since this is
|
|
24
|
+
* polled periodically at a much lower priority than the one-time transport check and there's no value in coupling
|
|
25
|
+
* the two together. */
|
|
26
|
+
export declare function collectDiagnostics(pc: Pick<RTCPeerConnection, "getStats">): Promise<ConnectionDiagnostics>;
|
|
14
27
|
export declare const createBrowserPeerConnection: RTCPeerConnectionFactory;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Whether the pair ICE settled on goes through a TURN relay
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Whether the pair ICE settled on goes through a TURN relay, and - when it's our own end that's relayed - whether
|
|
3
|
+
* that relay is TCP (`turn-tcp`, meaning `turns:` too, TLS being TCP-based as well) rather than UDP. The selected
|
|
4
|
+
* pair is found the standard way (the `transport` report's `selectedCandidatePairId`) and, for a browser that does
|
|
5
|
+
* not fill that in, as the succeeded and nominated `candidate-pair`. A relay on *either* end means the media
|
|
6
|
+
* passes through a TURN server, so it counts as at least `"turn"`; `"unknown"` when no pair is reported at all.
|
|
7
|
+
*
|
|
8
|
+
* The TCP/UDP distinction matters because they fail very differently under loss: UDP just drops the lost packet
|
|
9
|
+
* (a click), while TCP retransmits it and blocks everything queued behind it until that arrives (a stall) - exactly
|
|
10
|
+
* the "gaps that feel like latency, not packet loss" a participant on a TCP-relayed connection reports. `relayProtocol`
|
|
11
|
+
* (`"udp"`/`"tcp"`/`"tls"`) is only ever reported on a *local* relay candidate - there's no equivalent visibility into
|
|
12
|
+
* how the *remote* peer reaches its own TURN allocation, so a relay on the remote end alone is reported as plain
|
|
13
|
+
* `"turn"` rather than guessed at.
|
|
6
14
|
*/
|
|
7
15
|
export async function selectedConnectionType(pc) {
|
|
8
16
|
const reports = new Map();
|
|
@@ -15,7 +23,35 @@ export async function selectedConnectionType(pc) {
|
|
|
15
23
|
}
|
|
16
24
|
const local = reports.get(String(pair.localCandidateId));
|
|
17
25
|
const remote = reports.get(String(pair.remoteCandidateId));
|
|
18
|
-
|
|
26
|
+
if (local?.candidateType !== "relay" && remote?.candidateType !== "relay") {
|
|
27
|
+
return "p2p";
|
|
28
|
+
}
|
|
29
|
+
const relayProtocol = local?.candidateType === "relay" ? String(local.relayProtocol ?? "") : "";
|
|
30
|
+
return relayProtocol === "tcp" || relayProtocol === "tls" ? "turn-tcp" : "turn";
|
|
31
|
+
}
|
|
32
|
+
function streamDiagnostics(reports, kind) {
|
|
33
|
+
const inbound = [...reports.values()].find((report) => report.type === "inbound-rtp" && report.kind === kind);
|
|
34
|
+
const outbound = [...reports.values()].find((report) => report.type === "outbound-rtp" && report.kind === kind);
|
|
35
|
+
const num = (value) => (typeof value === "number" ? value : undefined);
|
|
36
|
+
return {
|
|
37
|
+
packetsLost: num(inbound?.packetsLost),
|
|
38
|
+
jitter: num(inbound?.jitter),
|
|
39
|
+
bytesSent: num(outbound?.bytesSent),
|
|
40
|
+
bytesReceived: num(inbound?.bytesReceived),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** One point-in-time sample of this connection's own quality stats, for the diagnostics panel - a single
|
|
44
|
+
* `getStats()` walk, independent of (and in addition to) `selectedConnectionType()`'s own call, since this is
|
|
45
|
+
* polled periodically at a much lower priority than the one-time transport check and there's no value in coupling
|
|
46
|
+
* the two together. */
|
|
47
|
+
export async function collectDiagnostics(pc) {
|
|
48
|
+
const reports = new Map();
|
|
49
|
+
(await pc.getStats()).forEach((report) => reports.set(String(report.id), report));
|
|
50
|
+
const transport = [...reports.values()].find((report) => report.type === "transport" && report.selectedCandidatePairId);
|
|
51
|
+
const pair = (transport ? reports.get(String(transport.selectedCandidatePairId)) : undefined) ??
|
|
52
|
+
[...reports.values()].find((report) => report.type === "candidate-pair" && (report.selected || (report.nominated && report.state === "succeeded")));
|
|
53
|
+
const roundTripTimeSeconds = typeof pair?.currentRoundTripTime === "number" ? pair.currentRoundTripTime : undefined;
|
|
54
|
+
return { roundTripTimeSeconds, audio: streamDiagnostics(reports, "audio"), video: streamDiagnostics(reports, "video") };
|
|
19
55
|
}
|
|
20
56
|
export const createBrowserPeerConnection = (config) => {
|
|
21
57
|
const pc = new RTCPeerConnection(config);
|
|
@@ -42,6 +78,7 @@ export const createBrowserPeerConnection = (config) => {
|
|
|
42
78
|
addIceCandidate: (candidate) => pc.addIceCandidate(candidate),
|
|
43
79
|
close: () => pc.close(),
|
|
44
80
|
connectionType: () => selectedConnectionType(pc),
|
|
81
|
+
collectDiagnostics: () => collectDiagnostics(pc),
|
|
45
82
|
onicecandidate: null,
|
|
46
83
|
ontrack: null,
|
|
47
84
|
onconnectionstatechange: null,
|
|
@@ -22,13 +22,20 @@ export declare const REACTION_EMOJIS: readonly ["👍", "👏", "❤️", "🎉"
|
|
|
22
22
|
* grows other push consumers) can cheaply recognize and skip anything that isn't one of these.
|
|
23
23
|
*
|
|
24
24
|
* `hello`/`bye`/`presenter-claim`/`presenter-release`/`state`/`reaction` are broadcast (`to` left unset - every
|
|
25
|
-
* participant is relevant); `offer`/`answer`/`ice-candidate` are point-to-point (`to` is the
|
|
26
|
-
* a recipient that isn't `to` (when set) ignores the message, since a channel's pub/sub fans
|
|
27
|
-
* every subscriber, sender included.
|
|
25
|
+
* participant is relevant); `offer`/`answer`/`ice-candidate`/`mute-request`/`kicked` are point-to-point (`to` is the
|
|
26
|
+
* intended peer's uid) - a recipient that isn't `to` (when set) ignores the message, since a channel's pub/sub fans
|
|
27
|
+
* every message out to every subscriber, sender included.
|
|
28
|
+
*
|
|
29
|
+
* `mute-request`/`kicked` are host moderation signals (see `MeshConnectionManager`'s doc comment) - purely
|
|
30
|
+
* cooperative, like every other message here: the recipient's own client decides whether to honor one, and nothing
|
|
31
|
+
* on the wire distinguishes a genuine host's message from any other participant's, since this client protocol has
|
|
32
|
+
* no host identity of its own to check against server-side. `kicked` is paired with the sender also revoking the
|
|
33
|
+
* recipient's server-side channel grant (`BaseVideoMeetingRoute.revokeChannelGrant()`), which is enforced and does
|
|
34
|
+
* not depend on the recipient's client cooperating.
|
|
28
35
|
*/
|
|
29
36
|
export interface SignalMessage {
|
|
30
37
|
type: "video-meeting-signal";
|
|
31
|
-
kind: "hello" | "bye" | "offer" | "answer" | "ice-candidate" | "presenter-claim" | "presenter-release" | "state" | "reaction" | "relay-fallback";
|
|
38
|
+
kind: "hello" | "bye" | "offer" | "answer" | "ice-candidate" | "presenter-claim" | "presenter-release" | "state" | "reaction" | "relay-fallback" | "mute-request" | "kicked";
|
|
32
39
|
/** The sender's authenticated uid - the real caller's own uid when `join()` returned `authenticated: true`, else
|
|
33
40
|
* the `guest:<random>` uid `BaseVideoMeetingRoute.join()` minted. The server refuses a published message whose
|
|
34
41
|
* `from` isn't the authenticated caller's uid (it stops one participant speaking as another), so this must be
|
|
@@ -62,6 +69,26 @@ export interface SignalingChannel {
|
|
|
62
69
|
* rather than relying on the transport to). Returns the function that stops listening. */
|
|
63
70
|
onMessage(handler: (message: SignalMessage) => void): () => void;
|
|
64
71
|
}
|
|
72
|
+
/** One kind's RTP stream stats, as of one sample - cumulative counters (`bytesSent`/`bytesReceived`), not a rate,
|
|
73
|
+
* since a single `getStats()` snapshot has no "since when" to divide by; a caller wanting a bitrate keeps the
|
|
74
|
+
* previous sample itself and divides the difference by the elapsed time (see `MeshConnectionManager`'s poller).
|
|
75
|
+
* `undefined` for anything this browser (or this stream's direction - an audio-only connection reports nothing for
|
|
76
|
+
* video, and vice versa) doesn't report. */
|
|
77
|
+
export interface RtpStreamDiagnostics {
|
|
78
|
+
packetsLost?: number;
|
|
79
|
+
/** Seconds. */
|
|
80
|
+
jitter?: number;
|
|
81
|
+
bytesSent?: number;
|
|
82
|
+
bytesReceived?: number;
|
|
83
|
+
}
|
|
84
|
+
/** One point-in-time sample of a connection's own quality stats, for the diagnostics panel. */
|
|
85
|
+
export interface ConnectionDiagnostics {
|
|
86
|
+
/** Seconds - the selected candidate pair's own current round-trip time. `undefined` when this browser doesn't
|
|
87
|
+
* report one (a connection still connecting, or simply not supported). */
|
|
88
|
+
roundTripTimeSeconds?: number;
|
|
89
|
+
audio: RtpStreamDiagnostics;
|
|
90
|
+
video: RtpStreamDiagnostics;
|
|
91
|
+
}
|
|
65
92
|
/** The subset of `RTCRtpSender` `MeshConnectionManager.setLocalTrack()` uses. Every connection carries one audio and
|
|
66
93
|
* one video sender for its whole life (see `MeshConnectionManager`'s doc comment), so turning a camera on or off,
|
|
67
94
|
* switching a device or sharing a screen only ever replaces the track a sender sends - no renegotiation. A real
|
|
@@ -101,16 +128,23 @@ export interface RTCPeerConnectionLike {
|
|
|
101
128
|
connectionState: string;
|
|
102
129
|
/** Which path the connected pair is using - read from the selected candidate pair once `connectionState` is
|
|
103
130
|
* `connected`. `"unknown"` when the browser does not report one (treated as direct by the caller). */
|
|
104
|
-
connectionType(): Promise<"p2p" | "turn" | "unknown">;
|
|
131
|
+
connectionType(): Promise<"p2p" | "turn" | "turn-tcp" | "unknown">;
|
|
132
|
+
/** One point-in-time sample of this connection's own quality stats, for the diagnostics panel - polled
|
|
133
|
+
* periodically and independently of `connectionType()` (see `MeshConnectionManager`'s doc comment). */
|
|
134
|
+
collectDiagnostics(): Promise<ConnectionDiagnostics>;
|
|
105
135
|
}
|
|
106
136
|
/** Which of the three media paths a participant's audio and video currently take, in the order they are tried:
|
|
107
137
|
*
|
|
108
138
|
* - `"p2p"`: a direct peer-to-peer WebRTC connection, the preferred path.
|
|
109
139
|
* - `"turn"`: still WebRTC, but relayed through the TURN server, for a participant a direct path cannot reach.
|
|
140
|
+
* - `"turn-tcp"`: the same, but the relay hop itself is TCP (or `turns:`, TLS being TCP-based too) rather than UDP -
|
|
141
|
+
* a network that blocks UDP outright. TCP's reliable, ordered delivery means one lost or delayed packet stalls
|
|
142
|
+
* everything queued behind it instead of just being dropped, so this can sound like pauses rather than the clicks
|
|
143
|
+
* plain packet loss would - worth telling apart from ordinary `"turn"` for exactly that reason.
|
|
110
144
|
* - `"websocket"`: not WebRTC at all - encoded frames proxied by this server over a WebSocket (see
|
|
111
145
|
* `apps/shared/relay/`), the last resort when neither of the above connects.
|
|
112
146
|
* - `"connecting"`: no path established yet. `"failed"`: every path this browser can try has failed. */
|
|
113
|
-
export type MediaTransport = "connecting" | "p2p" | "turn" | "websocket" | "failed";
|
|
147
|
+
export type MediaTransport = "connecting" | "p2p" | "turn" | "turn-tcp" | "websocket" | "failed";
|
|
114
148
|
/** What `MeshConnectionManager` needs from the WebSocket media relay (`apps/shared/relay/RelayTransport.ts` implements
|
|
115
149
|
* it) - declared here so the mesh, and its tests, depend on the shape alone. */
|
|
116
150
|
export interface RelayTransportLike {
|
|
@@ -135,6 +169,10 @@ export interface MeshParticipant extends ParticipantState {
|
|
|
135
169
|
name: string;
|
|
136
170
|
/** How this participant's media currently reaches the local one - see `MediaTransport`. */
|
|
137
171
|
transport: MediaTransport;
|
|
172
|
+
/** This connection's most recently polled quality stats, for the diagnostics panel - `undefined` until the
|
|
173
|
+
* first poll completes (shortly after `transport` first leaves `"connecting"`), and never present at all for
|
|
174
|
+
* a `"websocket"`-relayed participant (there is no `RTCPeerConnection` to poll). */
|
|
175
|
+
diagnostics?: ConnectionDiagnostics;
|
|
138
176
|
}
|
|
139
177
|
export type MeshEvent = {
|
|
140
178
|
type: "participant-joined";
|
|
@@ -161,4 +199,14 @@ export type MeshEvent = {
|
|
|
161
199
|
} | {
|
|
162
200
|
type: "presenter-changed";
|
|
163
201
|
uid: string | undefined;
|
|
202
|
+
}
|
|
203
|
+
/** Someone (implicitly the host, on the client's own say-so - see `SignalMessage`'s doc comment) asked the local
|
|
204
|
+
* participant to mute. The caller (`_CallView.tsx`) mutes only if currently unmuted; this never unmutes anyone. */
|
|
205
|
+
| {
|
|
206
|
+
type: "mute-requested";
|
|
207
|
+
}
|
|
208
|
+
/** The local participant was removed from the call. The caller tears down the mesh and shows a distinct
|
|
209
|
+
* "removed by the host" state rather than the ordinary "you left" one. */
|
|
210
|
+
| {
|
|
211
|
+
type: "kicked";
|
|
164
212
|
};
|
|
@@ -43,6 +43,9 @@ let VideoMeetingMongo = class VideoMeetingMongo extends BaseMongoEntity {
|
|
|
43
43
|
this.publicSlug = "publicSlug" in other ? other.publicSlug : this.publicSlug;
|
|
44
44
|
this.organizerSlug = "organizerSlug" in other ? other.organizerSlug : this.organizerSlug;
|
|
45
45
|
this.status = other.status !== undefined ? other.status : this.status;
|
|
46
|
+
this.forceMuteOnJoin = "forceMuteOnJoin" in other ? other.forceMuteOnJoin : this.forceMuteOnJoin;
|
|
47
|
+
this.passwordHash = "passwordHash" in other ? other.passwordHash : this.passwordHash;
|
|
48
|
+
this.waitingRoomEnabled = "waitingRoomEnabled" in other ? other.waitingRoomEnabled : this.waitingRoomEnabled;
|
|
46
49
|
this.startTime = "startTime" in other ? other.startTime : this.startTime;
|
|
47
50
|
this.endTime = "endTime" in other ? other.endTime : this.endTime;
|
|
48
51
|
}
|
|
@@ -86,6 +89,24 @@ __decorate([
|
|
|
86
89
|
Description("The current lifecycle state of this meeting."),
|
|
87
90
|
__metadata("design:type", String)
|
|
88
91
|
], VideoMeetingMongo.prototype, "status", void 0);
|
|
92
|
+
__decorate([
|
|
93
|
+
Column({ nullable: true }),
|
|
94
|
+
Description("When true, a joining participant starts with their microphone muted."),
|
|
95
|
+
Nullable,
|
|
96
|
+
__metadata("design:type", Boolean)
|
|
97
|
+
], VideoMeetingMongo.prototype, "forceMuteOnJoin", void 0);
|
|
98
|
+
__decorate([
|
|
99
|
+
Column({ nullable: true }),
|
|
100
|
+
Description("A hash of the password required to join, or unset when none is required. Never the plaintext."),
|
|
101
|
+
Nullable,
|
|
102
|
+
__metadata("design:type", String)
|
|
103
|
+
], VideoMeetingMongo.prototype, "passwordHash", void 0);
|
|
104
|
+
__decorate([
|
|
105
|
+
Column({ nullable: true }),
|
|
106
|
+
Description("When true, nobody but the organizer joins directly - everyone else waits for the host to admit them."),
|
|
107
|
+
Nullable,
|
|
108
|
+
__metadata("design:type", Boolean)
|
|
109
|
+
], VideoMeetingMongo.prototype, "waitingRoomEnabled", void 0);
|
|
89
110
|
__decorate([
|
|
90
111
|
Column({ nullable: true }),
|
|
91
112
|
Description("Informational only: not used for any availability or conflict checking."),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoMeetingMongo.js","sourceRoot":"","sources":["../../../../src/models/mongo/VideoMeetingMongo.ts"],"names":[],"mappings":";;;;;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACjH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAgB,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACvF,MAAM,EAAE,WAAW,EAAE,GAAG,aAAa,CAAC;AACtC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,eAAe,CAAC;AAC/C,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,qBAAqB,CAAC;AACxD,MAAM,EAAE,QAAQ,EAAE,GAAG,gBAAgB,CAAC;AAEtC;;;;;;;;GAQG;AAwBI,IAAM,iBAAiB,GAAvB,MAAM,iBAAkB,SAAQ,eAAe;
|
|
1
|
+
{"version":3,"file":"VideoMeetingMongo.js","sourceRoot":"","sources":["../../../../src/models/mongo/VideoMeetingMongo.ts"],"names":[],"mappings":"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"}
|
|
@@ -45,6 +45,9 @@ let VideoMeetingSQL = class VideoMeetingSQL extends BaseEntity {
|
|
|
45
45
|
this.publicSlug = "publicSlug" in other ? other.publicSlug : this.publicSlug;
|
|
46
46
|
this.organizerSlug = "organizerSlug" in other ? other.organizerSlug : this.organizerSlug;
|
|
47
47
|
this.status = other.status !== undefined ? other.status : this.status;
|
|
48
|
+
this.forceMuteOnJoin = "forceMuteOnJoin" in other ? other.forceMuteOnJoin : this.forceMuteOnJoin;
|
|
49
|
+
this.passwordHash = "passwordHash" in other ? other.passwordHash : this.passwordHash;
|
|
50
|
+
this.waitingRoomEnabled = "waitingRoomEnabled" in other ? other.waitingRoomEnabled : this.waitingRoomEnabled;
|
|
48
51
|
this.startTime = "startTime" in other ? other.startTime : this.startTime;
|
|
49
52
|
this.endTime = "endTime" in other ? other.endTime : this.endTime;
|
|
50
53
|
}
|
|
@@ -88,6 +91,24 @@ __decorate([
|
|
|
88
91
|
Description("The current lifecycle state of this meeting."),
|
|
89
92
|
__metadata("design:type", String)
|
|
90
93
|
], VideoMeetingSQL.prototype, "status", void 0);
|
|
94
|
+
__decorate([
|
|
95
|
+
Column({ nullable: true }),
|
|
96
|
+
Description("When true, a joining participant starts with their microphone muted."),
|
|
97
|
+
Nullable,
|
|
98
|
+
__metadata("design:type", Boolean)
|
|
99
|
+
], VideoMeetingSQL.prototype, "forceMuteOnJoin", void 0);
|
|
100
|
+
__decorate([
|
|
101
|
+
Column({ nullable: true }),
|
|
102
|
+
Description("A hash of the password required to join, or unset when none is required. Never the plaintext."),
|
|
103
|
+
Nullable,
|
|
104
|
+
__metadata("design:type", String)
|
|
105
|
+
], VideoMeetingSQL.prototype, "passwordHash", void 0);
|
|
106
|
+
__decorate([
|
|
107
|
+
Column({ nullable: true }),
|
|
108
|
+
Description("When true, nobody but the organizer joins directly - everyone else waits for the host to admit them."),
|
|
109
|
+
Nullable,
|
|
110
|
+
__metadata("design:type", Boolean)
|
|
111
|
+
], VideoMeetingSQL.prototype, "waitingRoomEnabled", void 0);
|
|
91
112
|
__decorate([
|
|
92
113
|
Column({ nullable: true }),
|
|
93
114
|
Description("Informational only: not used for any availability or conflict checking."),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoMeetingSQL.js","sourceRoot":"","sources":["../../../../src/models/sql/VideoMeetingSQL.ts"],"names":[],"mappings":";;;;;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAC5G,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAgB,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACvF,MAAM,EAAE,WAAW,EAAE,GAAG,aAAa,CAAC;AACtC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,eAAe,CAAC;AAC/C,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,qBAAqB,CAAC;AACxD,MAAM,EAAE,QAAQ,EAAE,GAAG,gBAAgB,CAAC;AAEtC;;;;;;;;GAQG;AAuBI,IAAM,eAAe,GAArB,MAAM,eAAgB,SAAQ,UAAU;
|
|
1
|
+
{"version":3,"file":"VideoMeetingSQL.js","sourceRoot":"","sources":["../../../../src/models/sql/VideoMeetingSQL.ts"],"names":[],"mappings":"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"}
|