@rapidmx/meet-plugin 0.13.1 → 0.14.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 +55 -55
- package/apps/meet/[token].tsx +341 -341
- package/apps/meet/_CallControls.tsx +463 -394
- package/apps/meet/_CallView.tsx +891 -848
- package/apps/meet/_DiagnosticsWindow.tsx +80 -0
- package/apps/meet/_MeetLobby.tsx +224 -224
- package/apps/meet/_ParticipantTile.tsx +169 -169
- package/apps/meet/_ParticipantsDrawer.tsx +10 -104
- package/apps/meet/_SettingsModal.tsx +152 -0
- package/apps/meet/_icons.tsx +80 -82
- package/apps/meet/_meetApi.ts +212 -212
- package/apps/shared/media/filters/VideoFilterProcessor.ts +408 -408
- package/apps/shared/media/filters/mlModels.ts +174 -174
- package/apps/shared/push/GuestSignalingClient.ts +359 -359
- package/apps/shared/relay/RelayClient.ts +7 -2
- package/apps/shared/webrtc/MeshConnectionManager.ts +949 -795
- package/apps/shared/webrtc/types.ts +247 -222
- package/dist/apps/meet/_CallControls.d.ts +19 -9
- package/dist/apps/meet/_CallControls.js +28 -10
- package/dist/apps/meet/_CallView.d.ts +14 -2
- package/dist/apps/meet/_CallView.js +27 -2
- package/dist/apps/meet/_DiagnosticsWindow.d.ts +25 -0
- package/dist/apps/meet/_DiagnosticsWindow.js +45 -0
- package/dist/apps/meet/_ParticipantsDrawer.d.ts +10 -21
- package/dist/apps/meet/_ParticipantsDrawer.js +9 -39
- package/dist/apps/meet/_SettingsModal.d.ts +28 -0
- package/dist/apps/meet/_SettingsModal.js +59 -0
- package/dist/apps/meet/_icons.d.ts +1 -1
- package/dist/apps/meet/_icons.js +1 -1
- package/dist/apps/shared/relay/RelayClient.d.ts +6 -1
- package/dist/apps/shared/relay/RelayClient.js +7 -2
- package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +55 -2
- package/dist/apps/shared/webrtc/MeshConnectionManager.js +153 -9
- package/dist/apps/shared/webrtc/types.d.ts +25 -1
- package/dist/lib/util/RedisRelayBus.js +61 -5
- package/dist/lib/util/RedisRelayBus.js.map +1 -1
- package/dist/lib/util/RelayHub.js +10 -3
- package/dist/lib/util/RelayHub.js.map +1 -1
- package/dist/types/util/RedisRelayBus.d.ts +45 -6
- package/dist/types/util/RelayHub.d.ts +9 -2
- package/package.json +208 -208
|
@@ -1,222 +1,247 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
/** Shared types for `MeshConnectionManager.ts` and its signaling transport (`apps/shared/push/GuestSignalingClient.ts`) -
|
|
6
|
-
* kept in their own module so a test can depend on the shapes alone. `GuestSignalingClient` itself now also serves
|
|
7
|
-
* an already-authenticated real caller (`VideoMeetingJoinResult.authenticated`), not only a synthetic guest - see
|
|
8
|
-
* its own doc comment. */
|
|
9
|
-
|
|
10
|
-
/** What a participant is sending, and whether their hand is up. Announced in `hello` and re-sent as a `state`
|
|
11
|
-
* message on every change, so a tile can show a muted microphone, an avatar for a camera that is off, and a raised
|
|
12
|
-
* hand without inspecting the media itself (the far end of a `replaceTrack(null)` gives no dependable signal that
|
|
13
|
-
* the track stopped). */
|
|
14
|
-
export interface ParticipantState {
|
|
15
|
-
audioOn: boolean;
|
|
16
|
-
videoOn: boolean;
|
|
17
|
-
handRaised: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** The emoji a participant can send during a call. A received `reaction` outside this list is ignored, so an
|
|
21
|
-
* arbitrary string from another participant is never rendered. */
|
|
22
|
-
export const REACTION_EMOJIS = ["👍", "👏", "❤️", "🎉", "😂", "😮", "😢", "👋"] as const;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
export interface
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* `
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
*
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/** Shared types for `MeshConnectionManager.ts` and its signaling transport (`apps/shared/push/GuestSignalingClient.ts`) -
|
|
6
|
+
* kept in their own module so a test can depend on the shapes alone. `GuestSignalingClient` itself now also serves
|
|
7
|
+
* an already-authenticated real caller (`VideoMeetingJoinResult.authenticated`), not only a synthetic guest - see
|
|
8
|
+
* its own doc comment. */
|
|
9
|
+
|
|
10
|
+
/** What a participant is sending, and whether their hand is up. Announced in `hello` and re-sent as a `state`
|
|
11
|
+
* message on every change, so a tile can show a muted microphone, an avatar for a camera that is off, and a raised
|
|
12
|
+
* hand without inspecting the media itself (the far end of a `replaceTrack(null)` gives no dependable signal that
|
|
13
|
+
* the track stopped). */
|
|
14
|
+
export interface ParticipantState {
|
|
15
|
+
audioOn: boolean;
|
|
16
|
+
videoOn: boolean;
|
|
17
|
+
handRaised: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The emoji a participant can send during a call. A received `reaction` outside this list is ignored, so an
|
|
21
|
+
* arbitrary string from another participant is never rendered. */
|
|
22
|
+
export const REACTION_EMOJIS = ["👍", "👏", "❤️", "🎉", "😂", "😮", "😢", "👋"] as const;
|
|
23
|
+
|
|
24
|
+
/** Which media path this participant's own tab currently insists on, for every peer - see
|
|
25
|
+
* `MeshConnectionManager.setTransportMode()`'s doc comment for what each one actually does:
|
|
26
|
+
*
|
|
27
|
+
* - `"auto"`: the default waterfall (direct, then TURN, then the server's own WebSocket relay) - unchanged behavior.
|
|
28
|
+
* - `"p2p"`: direct connections only - no TURN server offered to ICE, and a pair that can't connect directly stays
|
|
29
|
+
* `"failed"` rather than falling back to the WebSocket relay.
|
|
30
|
+
* - `"relay"`: TURN-relayed WebRTC only (`RTCPeerConnection`'s own `iceTransportPolicy: "relay"`) - still real
|
|
31
|
+
* WebRTC, just never direct; a pair with no reachable TURN server stays `"failed"`, same "no further fallback" rule
|
|
32
|
+
* as `"p2p"`.
|
|
33
|
+
* - `"websocket"`: skips WebRTC negotiation entirely for every pair and goes straight to the server's own relay.
|
|
34
|
+
*
|
|
35
|
+
* A purely personal, per-tab troubleshooting choice - never transmitted as a setting, and independent of whatever
|
|
36
|
+
* every other participant has chosen for their own tab. */
|
|
37
|
+
export type TransportMode = "auto" | "p2p" | "relay" | "websocket";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One WebRTC signaling message, published as the JSON body of `POST /push/:meetingUid` (`BasePushRoute.send()`)
|
|
41
|
+
* and received back over `/push`'s WebSocket, exactly as `BaseVideoMeetingRoute`'s doc comment describes ("SDP
|
|
42
|
+
* offers/answers and ICE candidates are ordinary `NotificationUtils.sendMessage()` payloads"). `type` is a fixed
|
|
43
|
+
* literal so a listener on the meeting's channel (which will also see other frame shapes if this server ever
|
|
44
|
+
* grows other push consumers) can cheaply recognize and skip anything that isn't one of these.
|
|
45
|
+
*
|
|
46
|
+
* `hello`/`bye`/`presenter-claim`/`presenter-release`/`state`/`reaction`/`talking-stick` are broadcast (`to` left
|
|
47
|
+
* unset - every participant is relevant); `offer`/`answer`/`ice-candidate`/`mute-request`/`kicked` are
|
|
48
|
+
* point-to-point (`to` is the intended peer's uid) - a recipient that isn't `to` (when set) ignores the message,
|
|
49
|
+
* since a channel's pub/sub fans every message out to every subscriber, sender included.
|
|
50
|
+
*
|
|
51
|
+
* `mute-request`/`kicked`/`talking-stick` are host moderation signals (see `MeshConnectionManager`'s doc comment) -
|
|
52
|
+
* purely cooperative, like every other message here: the recipient's own client decides whether to honor one, and
|
|
53
|
+
* nothing on the wire distinguishes a genuine host's message from any other participant's, since this client
|
|
54
|
+
* protocol has no host identity of its own to check against server-side. `kicked` is paired with the sender also
|
|
55
|
+
* revoking the recipient's server-side channel grant (`BaseVideoMeetingRoute.revokeChannelGrant()`), which is
|
|
56
|
+
* enforced and does not depend on the recipient's client cooperating; `talking-stick` has no server-side
|
|
57
|
+
* counterpart at all (see this module's doc comment on why it is purely an in-call runtime signal).
|
|
58
|
+
*
|
|
59
|
+
* `restart-connection` is broadcast whenever a participant changes their own `TransportMode`
|
|
60
|
+
* (`MeshConnectionManager.setTransportMode()`): every connection of theirs needs rebuilding under the new policy,
|
|
61
|
+
* and a recipient can't rebuild its own half of a pair unilaterally without the other side doing the same at
|
|
62
|
+
* (almost) the same moment, or the two sides end up negotiating against two different `RTCPeerConnection`s. No
|
|
63
|
+
* payload beyond the envelope - `from` already says whose connection to rebuild.
|
|
64
|
+
*/
|
|
65
|
+
export interface SignalMessage {
|
|
66
|
+
type: "video-meeting-signal";
|
|
67
|
+
kind:
|
|
68
|
+
| "hello"
|
|
69
|
+
| "bye"
|
|
70
|
+
| "offer"
|
|
71
|
+
| "answer"
|
|
72
|
+
| "ice-candidate"
|
|
73
|
+
| "presenter-claim"
|
|
74
|
+
| "presenter-release"
|
|
75
|
+
| "state"
|
|
76
|
+
| "reaction"
|
|
77
|
+
| "relay-fallback"
|
|
78
|
+
| "mute-request"
|
|
79
|
+
| "kicked"
|
|
80
|
+
| "talking-stick"
|
|
81
|
+
| "restart-connection";
|
|
82
|
+
/** The sender's authenticated uid - the real caller's own uid when `join()` returned `authenticated: true`, else
|
|
83
|
+
* the `guest:<random>` uid `BaseVideoMeetingRoute.join()` minted. The server refuses a published message whose
|
|
84
|
+
* `from` isn't the authenticated caller's uid (it stops one participant speaking as another), so this must be
|
|
85
|
+
* exactly that uid. */
|
|
86
|
+
from: string;
|
|
87
|
+
/** Which of the sender's tabs or devices this is: `<from>~<random>`. The same account joining from two devices is
|
|
88
|
+
* two participants, not one that ignores its own messages. A `peer` that doesn't start with `<from>~` is
|
|
89
|
+
* ignored, so it can't name someone else's tab. Absent from a sender that predates it (the participant is then
|
|
90
|
+
* identified by `from`). */
|
|
91
|
+
peer?: string;
|
|
92
|
+
/** Set only on a point-to-point message (`offer`/`answer`/`ice-candidate`/`relay-fallback`). */
|
|
93
|
+
to?: string;
|
|
94
|
+
/** `hello` only - the display name the sender chose in the lobby. */
|
|
95
|
+
name?: string;
|
|
96
|
+
/** `hello` and `state` - what the sender is currently sending and whether their hand is up. */
|
|
97
|
+
state?: ParticipantState;
|
|
98
|
+
/** `reaction` only - one of `REACTION_EMOJIS`. */
|
|
99
|
+
emoji?: string;
|
|
100
|
+
/** `talking-stick` only - whether talking-stick mode is currently on. */
|
|
101
|
+
active?: boolean;
|
|
102
|
+
/** `talking-stick` only - the peer id (`MeshParticipant.uid`/this tab's own id) currently holding the stick.
|
|
103
|
+
* Absent when `active` is `false`, or when nobody holds it yet. */
|
|
104
|
+
holder?: string;
|
|
105
|
+
/** `offer`/`answer` only. */
|
|
106
|
+
sdp?: RTCSessionDescriptionInit;
|
|
107
|
+
/** `ice-candidate` only. */
|
|
108
|
+
candidate?: RTCIceCandidateInit;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** What `MeshConnectionManager` needs from a signaling transport - implemented by
|
|
112
|
+
* `apps/shared/push/GuestSignalingClient.ts` for a real call, and by an in-memory fake for `MeshConnectionManager`'s own
|
|
113
|
+
* tests (see `test/webrtc/MeshConnectionManager.test.ts`), which never opens a real network connection. */
|
|
114
|
+
export interface SignalingChannel {
|
|
115
|
+
send(message: SignalMessage): void;
|
|
116
|
+
/** Calls `handler` with every message received on the channel (including, per the pub/sub fan-out this
|
|
117
|
+
* transport is built on, this same client's own sends - `MeshConnectionManager` filters those out itself
|
|
118
|
+
* rather than relying on the transport to). Returns the function that stops listening. */
|
|
119
|
+
onMessage(handler: (message: SignalMessage) => void): () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** One kind's RTP stream stats, as of one sample - cumulative counters (`bytesSent`/`bytesReceived`), not a rate,
|
|
123
|
+
* since a single `getStats()` snapshot has no "since when" to divide by; a caller wanting a bitrate keeps the
|
|
124
|
+
* previous sample itself and divides the difference by the elapsed time (see `MeshConnectionManager`'s poller).
|
|
125
|
+
* `undefined` for anything this browser (or this stream's direction - an audio-only connection reports nothing for
|
|
126
|
+
* video, and vice versa) doesn't report. */
|
|
127
|
+
export interface RtpStreamDiagnostics {
|
|
128
|
+
packetsLost?: number;
|
|
129
|
+
/** Seconds. */
|
|
130
|
+
jitter?: number;
|
|
131
|
+
bytesSent?: number;
|
|
132
|
+
bytesReceived?: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One point-in-time sample of a connection's own quality stats, for the diagnostics panel. */
|
|
136
|
+
export interface ConnectionDiagnostics {
|
|
137
|
+
/** Seconds - the selected candidate pair's own current round-trip time. `undefined` when this browser doesn't
|
|
138
|
+
* report one (a connection still connecting, or simply not supported). */
|
|
139
|
+
roundTripTimeSeconds?: number;
|
|
140
|
+
audio: RtpStreamDiagnostics;
|
|
141
|
+
video: RtpStreamDiagnostics;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The subset of `RTCRtpSender` `MeshConnectionManager.setLocalTrack()` uses. Every connection carries one audio and
|
|
145
|
+
* one video sender for its whole life (see `MeshConnectionManager`'s doc comment), so turning a camera on or off,
|
|
146
|
+
* switching a device or sharing a screen only ever replaces the track a sender sends - no renegotiation. A real
|
|
147
|
+
* `RTCRtpSender` already satisfies this shape. */
|
|
148
|
+
export interface RTCRtpSenderLike {
|
|
149
|
+
track: MediaStreamTrack | null;
|
|
150
|
+
replaceTrack(track: MediaStreamTrack | null): Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The subset of `RTCPeerConnection` `MeshConnectionManager` uses - what a test's fake implements, and what
|
|
154
|
+
* `apps/shared/webrtc/realPeerConnection.ts` adapts the real browser constructor to. Event handlers are plain settable
|
|
155
|
+
* properties (`RTCPeerConnection`'s own `on*` style) rather than `addEventListener`, matching the browser API
|
|
156
|
+
* this stands in for and keeping a test fake to the bare minimum. */
|
|
157
|
+
export interface RTCPeerConnectionLike {
|
|
158
|
+
/** The offerer's side: adds a send-and-receive transceiver for `kind`, sending `track` when there is one and
|
|
159
|
+
* nothing until `replaceTrack()` supplies it otherwise. */
|
|
160
|
+
addTransceiver(kind: "audio" | "video", track: MediaStreamTrack | null): { sender: RTCRtpSenderLike };
|
|
161
|
+
/** The answerer's side: once the remote offer is applied, turns the transceivers that offer created into
|
|
162
|
+
* send-and-receive ones and returns their senders by kind. (A browser only matches an offer's m-lines to
|
|
163
|
+
* transceivers it created for the offer itself - one the answerer added up front with `addTransceiver()` is left
|
|
164
|
+
* unused, and everything the answerer sends is silently lost.) */
|
|
165
|
+
claimTransceivers(): Partial<Record<"audio" | "video", RTCRtpSenderLike>>;
|
|
166
|
+
createOffer(): Promise<RTCSessionDescriptionInit>;
|
|
167
|
+
createAnswer(): Promise<RTCSessionDescriptionInit>;
|
|
168
|
+
setLocalDescription(description: RTCSessionDescriptionInit): Promise<void>;
|
|
169
|
+
setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void>;
|
|
170
|
+
addIceCandidate(candidate: RTCIceCandidateInit): Promise<void>;
|
|
171
|
+
close(): void;
|
|
172
|
+
onicecandidate: ((event: { candidate: RTCIceCandidateInit | null }) => void) | null;
|
|
173
|
+
ontrack: ((event: { track: MediaStreamTrack }) => void) | null;
|
|
174
|
+
onconnectionstatechange: (() => void) | null;
|
|
175
|
+
connectionState: string;
|
|
176
|
+
/** Which path the connected pair is using - read from the selected candidate pair once `connectionState` is
|
|
177
|
+
* `connected`. `"unknown"` when the browser does not report one (treated as direct by the caller). */
|
|
178
|
+
connectionType(): Promise<"p2p" | "turn" | "turn-tcp" | "unknown">;
|
|
179
|
+
/** One point-in-time sample of this connection's own quality stats, for the diagnostics panel - polled
|
|
180
|
+
* periodically and independently of `connectionType()` (see `MeshConnectionManager`'s doc comment). */
|
|
181
|
+
collectDiagnostics(): Promise<ConnectionDiagnostics>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Which of the three media paths a participant's audio and video currently take, in the order they are tried:
|
|
185
|
+
*
|
|
186
|
+
* - `"p2p"`: a direct peer-to-peer WebRTC connection, the preferred path.
|
|
187
|
+
* - `"turn"`: still WebRTC, but relayed through the TURN server, for a participant a direct path cannot reach.
|
|
188
|
+
* - `"turn-tcp"`: the same, but the relay hop itself is TCP (or `turns:`, TLS being TCP-based too) rather than UDP -
|
|
189
|
+
* a network that blocks UDP outright. TCP's reliable, ordered delivery means one lost or delayed packet stalls
|
|
190
|
+
* everything queued behind it instead of just being dropped, so this can sound like pauses rather than the clicks
|
|
191
|
+
* plain packet loss would - worth telling apart from ordinary `"turn"` for exactly that reason.
|
|
192
|
+
* - `"websocket"`: not WebRTC at all - encoded frames proxied by this server over a WebSocket (see
|
|
193
|
+
* `apps/shared/relay/`), the last resort when neither of the above connects.
|
|
194
|
+
* - `"connecting"`: no path established yet. `"failed"`: every path this browser can try has failed. */
|
|
195
|
+
export type MediaTransport = "connecting" | "p2p" | "turn" | "turn-tcp" | "websocket" | "failed";
|
|
196
|
+
|
|
197
|
+
/** What `MeshConnectionManager` needs from the WebSocket media relay (`apps/shared/relay/RelayTransport.ts` implements
|
|
198
|
+
* it) - declared here so the mesh, and its tests, depend on the shape alone. */
|
|
199
|
+
export interface RelayTransportLike {
|
|
200
|
+
/** `false` when this browser cannot do it (no WebSocket, no WebCodecs) - the mesh then never falls back to it. */
|
|
201
|
+
readonly supported: boolean;
|
|
202
|
+
/** Starts receiving `peerId`'s media. `onStream` is given the `MediaStream` that plays it. */
|
|
203
|
+
receiveFrom(peerId: string, onStream: (stream: MediaStream) => void): void;
|
|
204
|
+
stopReceivingFrom(peerId: string): void;
|
|
205
|
+
/** Starts or stops publishing the local tracks to the relay. */
|
|
206
|
+
setSending(active: boolean): void;
|
|
207
|
+
/** Same as `MeshConnectionManager.setLocalTrack()` - `null` sends nothing for that kind. */
|
|
208
|
+
setLocalTrack(kind: "audio" | "video", track: MediaStreamTrack | null): void;
|
|
209
|
+
close(): void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** `iceTransportPolicy` mirrors the real `RTCConfiguration` field of the same name - `"relay"` forces ICE to use
|
|
213
|
+
* only a TURN relay candidate, never a direct one (`TransportMode.relay`); omitted (or `"all"`) is the ordinary,
|
|
214
|
+
* unconstrained default. */
|
|
215
|
+
export type RTCPeerConnectionFactory = (config: { iceServers: RTCIceServer[]; iceTransportPolicy?: "all" | "relay" }) => RTCPeerConnectionLike;
|
|
216
|
+
|
|
217
|
+
/** One other participant currently known to be in the call - `MeshConnectionManager.participants` never includes
|
|
218
|
+
* the local participant themselves (the UI already knows its own name/uid without asking the manager). */
|
|
219
|
+
export interface MeshParticipant extends ParticipantState {
|
|
220
|
+
uid: string;
|
|
221
|
+
name: string;
|
|
222
|
+
/** How this participant's media currently reaches the local one - see `MediaTransport`. */
|
|
223
|
+
transport: MediaTransport;
|
|
224
|
+
/** This connection's most recently polled quality stats, for the diagnostics panel - `undefined` until the
|
|
225
|
+
* first poll completes (shortly after `transport` first leaves `"connecting"`), and never present at all for
|
|
226
|
+
* a `"websocket"`-relayed participant (there is no `RTCPeerConnection` to poll). */
|
|
227
|
+
diagnostics?: ConnectionDiagnostics;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export type MeshEvent =
|
|
231
|
+
| { type: "participant-joined"; participant: MeshParticipant }
|
|
232
|
+
| { type: "participant-updated"; participant: MeshParticipant }
|
|
233
|
+
| { type: "participant-left"; uid: string }
|
|
234
|
+
| { type: "remote-stream"; uid: string; stream: MediaStream }
|
|
235
|
+
| { type: "hand-raised"; uid: string; name: string }
|
|
236
|
+
| { type: "reaction"; uid: string; name: string; emoji: string }
|
|
237
|
+
| { type: "presenter-changed"; uid: string | undefined }
|
|
238
|
+
/** Someone (implicitly the host, on the client's own say-so - see `SignalMessage`'s doc comment) asked the local
|
|
239
|
+
* participant to mute. The caller (`_CallView.tsx`) mutes only if currently unmuted; this never unmutes anyone. */
|
|
240
|
+
| { type: "mute-requested" }
|
|
241
|
+
/** The local participant was removed from the call. The caller tears down the mesh and shows a distinct
|
|
242
|
+
* "removed by the host" state rather than the ordinary "you left" one. */
|
|
243
|
+
| { type: "kicked" }
|
|
244
|
+
/** Talking-stick mode turned on or off, or the stick changed hands - see this module's doc comment and
|
|
245
|
+
* `MeshConnectionManager.setTalkingStick()`. `holder` is the peer id now holding it; absent while `active` is
|
|
246
|
+
* `false`, or (transiently) if whoever held it has since left the call. */
|
|
247
|
+
| { type: "talking-stick-changed"; active: boolean; holder?: string };
|
|
@@ -5,26 +5,28 @@
|
|
|
5
5
|
* screen, reactions, raise hand, grid/focus and leave. The effects button opens the video filter picker
|
|
6
6
|
* (`_EffectsPanel.tsx`) and lights up while any filter is on.
|
|
7
7
|
*
|
|
8
|
-
* The diagnostics button is the one exception to "opens near its own button": its panel is `fixed` to the
|
|
9
|
-
* viewport's top-left corner rather than anchored above the button, since it reads better as a HUD-style overlay
|
|
10
|
-
* sitting over the call than as a dropdown tucked into the bottom control bar. The outside-click/Escape handling
|
|
11
|
-
* (`barRef`) still closes it the same way, since it's still a DOM descendant of the bar - only its visual position
|
|
12
|
-
* moved, not its open/close wiring.
|
|
13
|
-
*
|
|
14
8
|
* The microphone button shows a live level while it is unmuted - bars that move with the sound the microphone is
|
|
15
9
|
* picking up, so a participant can see their audio is being sent - and the camera button shows a green dot while a
|
|
16
10
|
* camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
|
|
17
11
|
* is disabled (`micLocked`) while talking-stick mode is on and this participant isn't the current holder - see
|
|
18
12
|
* `_CallView.tsx`'s doc comment.
|
|
13
|
+
*
|
|
14
|
+
* ## The "…" menu
|
|
15
|
+
*
|
|
16
|
+
* Sits between the grid/focus toggle and Leave, a catch-all for controls that don't need a dedicated button of
|
|
17
|
+
* their own: "Diagnostics" (`onOpenDiagnostics`) and "Settings" (`onOpenSettings`, host-only) each open their own
|
|
18
|
+
* persistent window/modal in `_CallView.tsx` and close this menu; "Connection method" is a four-way
|
|
19
|
+
* `menuitemradio` group (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment)
|
|
20
|
+
* forcing how *this tab's* media reaches everyone else, independent of what anyone else has chosen. Otherwise an
|
|
21
|
+
* ordinary menu of this bar's own shape (`openMenu`, closes on Escape/outside click like every other one here).
|
|
19
22
|
*/
|
|
20
23
|
import React from "react";
|
|
21
24
|
import type { ScreenTransformState } from "../shared/media/filters/ScreenTransform.js";
|
|
22
25
|
import type { LocalMedia } from "../shared/media/useLocalMedia.js";
|
|
23
|
-
import { type MeshParticipant } from "../shared/webrtc/types.js";
|
|
26
|
+
import { type MeshParticipant, type TransportMode } from "../shared/webrtc/types.js";
|
|
24
27
|
export type CallViewMode = "grid" | "focus";
|
|
25
28
|
export interface CallControlsProps {
|
|
26
29
|
media: LocalMedia;
|
|
27
|
-
selfName: string;
|
|
28
30
|
participants: MeshParticipant[];
|
|
29
31
|
isPresenting: boolean;
|
|
30
32
|
/** Someone else is presenting - the share button is disabled and explains why. */
|
|
@@ -38,15 +40,23 @@ export interface CallControlsProps {
|
|
|
38
40
|
onToggleHand: () => void;
|
|
39
41
|
onReaction: (emoji: string) => void;
|
|
40
42
|
viewMode: CallViewMode;
|
|
43
|
+
/** Disabled whenever `participants.length < 2` - grid and focused view render the exact same single tile with
|
|
44
|
+
* only one other participant (or none), so there is nothing for this to visibly toggle yet. */
|
|
41
45
|
onToggleViewMode: () => void;
|
|
42
46
|
/** True while talking-stick mode is on and this participant doesn't currently hold it - see `_CallView.tsx`'s
|
|
43
47
|
* doc comment on talking-stick mode. Disables the microphone toggle (not the device-picker chevron, which
|
|
44
48
|
* doesn't change whether anyone can hear them) rather than merely nudging it, unlike a `mute-request`. */
|
|
45
49
|
micLocked?: boolean;
|
|
50
|
+
/** Whether the "…" menu's "Settings" item is offered at all - see this module's doc comment on the menu. */
|
|
51
|
+
isHost: boolean;
|
|
52
|
+
transportMode: TransportMode;
|
|
53
|
+
onSetTransportMode: (mode: TransportMode) => void;
|
|
54
|
+
onOpenDiagnostics: () => void;
|
|
55
|
+
onOpenSettings: () => void;
|
|
46
56
|
onLeave: () => void;
|
|
47
57
|
}
|
|
48
58
|
/** Bars that rise and fall with the microphone's level (0-5) - a flat, dim set while there is no sound. */
|
|
49
59
|
export declare function LevelBars({ level }: {
|
|
50
60
|
level: number;
|
|
51
61
|
}): React.JSX.Element;
|
|
52
|
-
export default function CallControls({ media,
|
|
62
|
+
export default function CallControls({ media, participants, isPresenting, presentingElsewhereName, onToggleShare, screenTransform, onRotateScreen, onFlipScreen, handRaised, onToggleHand, onReaction, viewMode, onToggleViewMode, micLocked, isHost, transportMode, onSetTransportMode, onOpenDiagnostics, onOpenSettings, onLeave, }: CallControlsProps): React.JSX.Element;
|